NestJS || TypeORM

Alpha, Orderly·2023년 9월 19일
0

nestJS

목록 보기
5/8
  • Typescript 를 통해 DB에 접근하는 방식
  • SQL / NoSQL 둘다 사용 가능하다.

설치

  • npm install @nestjs/typeorm typeorm

Entity

  • 사용자가 저장할 타입을 지정한다.
    • EX : User entity > ID, EMAIL 을 포함

TypeORM 사용하기

  1. 최상단 모듈에 TypeORM Module을 import 한다.
  2. 모듈의 imports에 설정과 함께 추가한다.
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite', 
      database: 'db.sqlite',
      synchronize: true
    }),
    UsersModule, 
    ReportsModule],
  controllers: [AppController],
  providers: [AppService],
})
Synchronize
  • 개발환경에서만 사용한다.
  • 엔티티를 확인해 데이터베이스의 구조를 업데이트한다.
  • 배포 환경에서 바로 엔티티의 변화가 DB로 적용될시 엔티티의 칼럼이 없어지면 DB에도 없어져 데이터의 손실이 생길수 있다!

Entity 생성하기

  • 타입에 해당하는 이름을 그대로 클래스 이름으로 쓴다.
@Entity()
export class User {
	// 자동으로 생성되는 칼럼
    // Primary Key 로 사용됨.
    @PrimaryGeneratedColumn()
    id: number;
    // 이하 DB Column
    @Column()
    email: string;
    @Column()
    password: string;
}
  • 생성된 엔티티를 모듈에 연결한다.
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService]
})
  • 최상단 모듈에 entities 로 추가한다.
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'db.sqlite',
      synchronize: true,
      entities: [User],
    }),
    UsersModule, 
    ReportsModule],
  controllers: [AppController],
  providers: [AppService],
})

Repository 사용하기

  • Repository를 사용해야 하는곳 ( Ex. 서비스 ) 에 아래와 같이 inject 한다.
    • InjectRepository 를 사용해 Entity의 Repository를 Inject 한다.
    constructor(
        @InjectRepository(User) private repo: Repository<User>
    ) {}
  • repo를 사용하는 예시는 아래와 같다.
    create(email: string, password: string) {
        const user = this.repo.create({email, password});

        return this.repo.save(user);
    }

Repository API 메소드

create()

  • 엔티티의 새로운 객체를 생성, DB에 저장하지는 않는다.
사용하는 이유 : 엔티티에 Validation 을 포함할 경우 이를 확인해준다.
또한 아래 After** 가 호출되게 하기 위해선 create를 통해 객체를 만들어서 사용해야 한다.

save()

  • DB에 레코드를 추가하거나 업데이트한다.
  • 추가 후 추가된 값을 리턴한다.
  • 값을 업데이트할때 save에 주어진 값만 업데이트하고 나머진 빈칸이 된다.
  1. insert()
    • 데이터를 저장하고 값이 있으면 duplicate 오류 발생
  2. update()
    • 변경하고픈 필드만 수정가능

find()

  • 쿼리를 실행해 엔티티 리스트를 리턴한다.

findOne()

  • 쿼리를 실행해 첫번째 값을 리턴한다.

remove()

  • DB에서 해당하는 레코드를 삭제한다.
  • 값이 없으면 에러를 리턴합니다.
  • 값의 유무를 먼저 확인하고 제거
  1. delete()
    • 값의 유무를 확인하지 않고 제거합니다.
    • 값이 없으면 아무런 동작을 하지 않습니다.

AfterInsert

  • 새로운 데이터를 넣었을때 어떤것이 실행될지 정할수 있다.
  • 엔티티에 선언
@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number;
    @Column()
    email: string;
    @Column()
    password: string;

    @AfterInsert()
    logInsert() {
        console.log(`Inserted User with ${this.id}`);
    }
}
  • 이 외에도 AfterRemove / AfterUpdate를 사용해 레코드 삭제 혹은 업데이트에도 대응할수 있다.

find 사용하기

  • 특정 레코드를 찾는데 이용된다.

where

  • 조건을 걸어 특정 레코드를 찾는다.
    find(email: string) {
        return this.repo.find({ where: { email }})
    }
  • 위 경우는 email 항목이 값이 같은 레코드를 찾게 된다.
    findOne(id: number) {
        return this.repo.findOneBy({id})
    }
  • 위 경우는 id를 이용해 하나만 찾는 경우이다.

update 사용하기

  • partial
    • 값을 업데이트 할때에는 엔티티의 일부만 필요로 할수 있다.
    • 이때 사용하는것이 partial이다.
    • User에게 email, password가 있더라도 아래와 같이 작성시 email 만 받게 할수 있다.
updateData(id: number, attrs: Partial<User>) {

}
    async update(id: number, attrs: Partial<User>) {
        const user = await this.findOne(id);
        if(!user) {
            throw new Error('user not found');
        }
        // attr의 값을 user에 대입한다.
        Object.assign(user, attrs)
        return this.repo.save(user);
    }

Dto 로 일부만 받아올수 있도록 하는법

  • isOptional 을 이용, Validation 절차에서 없어도 되도록 한다.

Class serializer interceptor

  • 컨트롤러에서 응답시 이를 가로채 특정 규칙을 적용한 object로 변환해 보낼수 있다.
  • 엔티티에 규칙을 적용해 사용한다.
    @Column()
    @Exclude()
    password: string;
  • class-transformer의 Exclude()를 특정 칼럼에 적용해
    응답 데이터에서 이를 빼고 보내도록 한다.
  • 또한 컨트롤러에도 지정이 필요하다.
  • @UseInterceptors(ClassSerializerInterceptor) 를 추가한다.
    @UseInterceptors(ClassSerializerInterceptor)
    @Get('/:id')
    async findUser(@Param('id') id: string) {
        const user = await this.usersService.findOne(parseInt(id));
        if (!user) throw new NotFoundException('user not found');
        return user;
    }

Custom Interceptor

  • 엔티티에 적용하지 않는다.

커스텀 인터셉터 만들기

class CustomInterceptor {
intercept(context: ExecutionContext, next: CallHandler);
}
  • intercept : 자동으로 호출되는 함수
  • context : 들어오는 요청에 대한 정보
  • next : 컨트롤러단 핸들러 ( 다음에 실행됨 )
  • 보통 serialize.interceptor.ts 로 이름짓는다.

예시

export class SerializeInterceptor implements NestInterceptor {

    constructor(private dto: any) {}

    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {

        return next.handle().pipe(
            map(
                (data: any) => {
                    return plainToInstance(UserDto, data, {
                        excludeExtraneousValues: true,
                    })
                }
            )
        )
    }
}
  • 사용은 그전과 같다.
    @UseInterceptors(SerializeInterceptor)
    @Get()
    findAllUsers(@Query('email') email: string) {
        return this.usersService.find(email);
    }
  • 이를 이용해 응답을 변환하기 위해선 Dto를 사용해야 한다.
  • 아래는 응답에 사용할 dto를 만든것, expose를 통해 표기할것을 적어놓는다.
import { Expose } from "class-transformer";

export class UserDto {
    @Expose()
    id: number;
    @Expose()
    email: string;
}
  • 응답 이후 코드를 아래와 같이 수정한다.
        return next.handle().pipe(
            map(
                (data: any) => {
                    return plainToInstance(UserDto, data, {
                        excludeExtraneousValues: true,
                    })
                }
            )
        )
    }
  • 이는 JSON 인 data를 UserDto로 변환하는데, 이 과정에서 @Expose가 되어있는 것만 변환하겠다는것을 포함한다
    • excludeExtraneousValues: true
export function Serialize(dto: any) {
    return UseInterceptors(new SerializeInterceptor(UserDto));
}

를 interceptor 코드에 추가하고

    @Serialize(UserDto)
    @Get()
    findAllUsers(@Query('email') email: string) {
        return this.usersService.find(email);
    }

와 같이 사용시 여러 종류의 Dto에 대응하도록 구현할수 있다.

컨트롤러 단에서 Intercept 하기

  • 위의 UseInterceptor 혹은 Serialize 데코레이터를 컨트롤러에 붙히면 된다

Relation / Association

  • 엔티티와 엔티티간 관계를 표현하기 위해 주로 사용한다.

One to One Relationship

  • 1대1로 대응되는 관계
    • 국가 <> 수도

One to Many / Many to One

  • 1대다로 대응되는 관계
    • 소비자 <> 주문
    • 차량 <> 부품
  • 앞이 자신, 뒤가 상대
  • One to Many 는 자신이 한개고 상대방이 여러개
  • 소비자 기준으로 주문이 여러개이므로 One To Many

Many to Many

  • 다대다로 대응되는 된계
    • 학년 <> 학생
    • 앨범 <> 장르

One to Many 예시

  • 한 유저는 여러개의 리포트를 가질수 있는것을 가정한다.
  • User 기준으로는 One to Many
  • Report 기준으로는 Many to One

User Entity

// report 에서 자신이 있는 부분을 알린다.
    @OneToMany(() => Report, (report) => report.user)
    reports: Report[];

Report Entity

// user에서 자신이 있는 부분을 알린다.
    @ManyToOne(() => User, (user) => user.reports)
    user: User;

포인트

  • 설정하지 않으면 유저나 리포트를 가져왔다고 관련된것까지 가져오지는 않는다.

테이블

  • OneToMany 는 테이블을 변경하지 않는다.
  • ManyToOne 은 테이블에 칼럼이 추가된다.

하나의 리포트에 한개 이상의 유저가 들어가려면?

  • Ex. 소유자와 승인자가 따로 있을수 있다.
  • @ManyToOne(() => User, (user) => user.reports)
    • 부분에서 user.report 부분을 적당히 수정해 추가한다.

쿼리빌더

  • repository 에서 createQueryBuilder() 를 통해 쿼리 호출이 가능하다.
  • 조건이 여러개면 두번째부터 andWhere로 사용한다.
  • orderBy는 따로 아래에 setParameter로 :id를 지정해주는 모습
  • getRawMany() 를 호출해 Raw 데이터로 가져온다.
    • 엔티티가 아니라 자유로운 형식으로 값을 리턴한다.
this.repo
.createQueryBuilder()
.select('*')
.where('make = :make', {make: brand}) // :make에 make key 의 값이 들어간다. SQL Inject 방지용.
.andWhere('model = :model', {model})
.orderBy(':id', 'DESC')
.setParameters({ id })
.getRawMany()

Migration File

  • npm install -D ts-node tsconfig-paths 설치
  • 루드 디렉토리에 파일을 만들고 아래와 같이 작성
import { ConfigService } from '@nestjs/config';
import { config } from 'dotenv';
import { DataSource } from 'typeorm';

config();

const configService = new ConfigService();

export default new DataSource({
  type: 'mysql',
  host: configService.get('DB_HOST'),
  port: configService.get<number>('DB_PORT'),
  username: configService.get('DB_USERNAME'),
  password: configService.get('DB_PASSWORD'),
  database: configService.get('DB_DATABASE'),
  synchronize: false,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/database/migrations/*.ts'],
  migrationsTableName: 'migrations',
});
  • package.json 의 script에 아래 추가
    "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --dataSource ./data-source.ts",
    "migration:create": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:create ./src/database/migrations/Migration",
    "migration:generate": "npm run typeorm migration:generate ./src/database/migrations/Migration",
    "migration:run": "npm run typeorm  migration:run",
    "migration:revert": "npm run typeorm migration:revert",

칼럼을 추가하는 경우

  • npm run migration:generate 명령어 실행시 위에 정해진 디렉토리에 시간-Migration.ts 가 생김
  • 이름에 시간이 붙기에, 안전하게 변경이 가능해짐.
  • 거기에 up / down 함수를 작성
import { MigrationInterface, QueryRunner } from 'typeorm';

export class Migration1675868794851 implements MigrationInterface {
  name = 'Migration1675868794851';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `ALTER TABLE \`cat\` ADD \`kind\` varchar(255) NOT NULL`,
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE \`cat\` DROP COLUMN \`kind\``);
  }
}
  • up : DB 변경
npm run migration:run
  • down : up의 변경사항 되돌림
npm run migration:revert
profile
만능 컴덕후 겸 번지 팬

0개의 댓글