개발일지

[NestJS] TypeORM 적용하기 본문

NestJS, Node.js/#01 Project - 투표 커뮤니티

[NestJS] TypeORM 적용하기

lyjin 2022. 12. 22.
  • 기존 프로젝트에 적용되었던 Prisma를 TypeORM으로 변경했습니다.
  • NestJS 공식 문서를 참고했습니다. 공식 문서에서는 MySQL을 사용하나 저는 SQLite를 사용했습니다.

TypeORM 적용

준비

필요한 모든 종속성 패키지를 설치합니다. 사용하려는 데이터베이스의 클라아이언트 API 라이브러리(sqlite3)를 함께 설치해야합니다.

 

$ yarn add --save @nestjs/typeorm typeorm sqlite3 --save

 

설치가 완료되면 TypeOrmModule을 가져올 수 있습니다.

 

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
      TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'vote-database',
      entities: [__dirname + '/../**/*.entity.{js,ts}'],
      synchronize: true,
    }),
  ],
})

export class AppModule {}
  • type: RDBMS 타입
  • database: 가져올 원시 UInt8Array 데이터 베이스. (sqlite 옵션)
  • entities: 사용될 entity 스키마 또는 클래스
  • synchronize: 애플리케이션 실행 시 스키마 자동 생성 여부. production 환경에서는 사용하면 안됩니다. 데이터가 손실될 수 있습니다.

더 많은 옵션은 여기를 참고해주세요.


Repository 패턴

TypeORM에서 지원하는 Repository 패턴을 사용했습니다. 먼저 모듈에 forFeature()로 현재 scope에 등록된 respository를 정의합니다. 그 후 @InjectRepository() 데코레이터로 의존성을 주입시킬 수 있습니다. 생성한 respository를 외부 모듈에서 사용하고 싶다면 TypeOrmModule을 export 하면 됩니다.

 

// users.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { RefreshTokensEntity, UsersEntity } from '@vote/common';

@Module({
  imports: [
    TypeOrmModule.forFeature([UsersEntity, RefreshTokensEntity]),
  ],
  exports: [TypeOrmModule],
  ...
})
export class UsersModule {}
// users.service.ts
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(UsersEntity)
    private readonly usersRepository: Repository<UsersEntity>,
    @InjectRepository(RefreshTokensEntity)
    private readonly refreshTokenRepository: Repository<RefreshTokensEntity>,
  ) {}
  ...
}

 


Entity 관련

Entity 상속

먼저 모든 모델의 공통 필드들만을 가지고 있는 entity를 생성했습니다.

 

export class CommonEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

 

다음과 같이 상속하여 사용합니다.

 

@Entity({ name: 'users' })
export class UsersEntity extends CommonEntity {
  ...
}

cascade 옵션

회원가입 시 해당 users 레코드를 참조하는 refresh_tokens(1:1) 레코드가 함께 생성되도록 구현하고 싶었습니다. 이를 위해 cascade 옵션을 사용했습니다. 다음과 같이 cascade: ['insert'] 옵션을 주면 users 모델이 save() 될 때 refresh_tokens 모델도 함께 저장됩니다.

 

//users.entity.ts

@Entity({ name: 'users' })
export class UsersEntity extends CommonEntity {
  @OneToOne(() => RefreshTokensEntity, (refreshToken) => refreshToken.user, {
    cascade: ['insert'], ⭐️
  })
  refreshToken?: RefreshTokensEntity;
}
// users.service.ts

  async createUser(dto: SignUpUserDto): Promise<UsersEntity> {
    const { email, nickname, password } = dto;
    const hashedPassword = await bcrypt.hash(password, 10);

    // to entity
    const entity = UsersEntity.from(email, nickname, hashedPassword);
    const userEntity = this.usersRepository.create(entity);

    // refreshToken
    const token = new RefreshTokensEntity();
    userEntity.refreshToken = token;

    return await this.usersRepository.save(userEntity);
  }

One-to-many create

투표 글 생성시 투표 선택지는 배열로 받은 뒤 vote-choices 모델에 각각의 레코드로 데이터베이스에 저장합니다.(1:n) 먼저 1:n 관계의 entity를 정의합니다.

 

// votes.entity.ts

@Entity({ name: 'votes' })
export class VotesEntity extends CommonEntity {
  @OneToMany((type) => VoteChoicesEntity, (voteChoices) => voteChoices.vote, {
    nullable: true,
    cascade: ['insert'],
  })
  @JoinTable()
  voteChoices: VoteChoicesEntity[];
}

@Entity({ name: 'vote-choices' })
export class VoteChoicesEntity extends CommonEntity {
  @ManyToOne((type) => VotesEntity, (vote) => vote.voteChoices, {
    onDelete: 'CASCADE',
  })
  vote: VotesEntity;

  @Column()
  title: string;

  @OneToMany((type) => ChoicedUsersEntity, (choiced) => choiced.choice, {
    eager: true,
  })
  @JoinTable()
  choiced: ChoicedUsersEntity[];
}

 

voteChoices는 VoteChoicesEntity[] 형태를 가져야하기 때문에 map() 함수를 사용했습니다. 위에 cascade 옵션에 따라 voteEntity가 save 될 때 voteChoices도 함께 저장됩니다.

 

// votes.service.ts

  async createVote(userId: number, dto: CreateVoteDto) {
    const { title, endDate, voteChoices } = dto;
    ...

    const voteEntity = this.votesRepository.create({
      title,
      endDate,
      writer,
      voteChoices: voteChoices.map((value) => {
        const choice = new VoteChoicesEntity();
        choice.title = value;
        return choice;
      }),
    });

    return await this.votesRepository.save(voteEntity);
  }

 


Many-to-many 테이블 커스텀

본래는 @ManyToMany()@JoinTable() 데코레이터를 사용하면 조인테이블을 한 번에 생성할 수 있습니다. 그러나 저는 유저당 한 게시물에 한 번만 투표 가능하도록 구현하게 위해 [vote-user] 중복 컬럼 고유 제약 조건을 추가하고 싶었고, 조인 테이블을 직접 생성해줬습니다.

 

//votes.entity.ts

@Entity({ name: 'votes_to_users' })
@Index('voted_user', ['vote', 'user'], { unique: true })
export class VotedUsersEntity extends CommonEntity {
  @ManyToOne((type) => VotesEntity, (vote) => vote.voted, {
    onDelete: 'CASCADE',
  })
  vote: VotesEntity;

  @ManyToOne((type) => UsersEntity, (user) => user.votedUsers, {
    onDelete: 'SET NULL',
  })
  user: UsersEntity;
}
// votes.entity.ts
@Entity({ name: 'votes' })
export class VotesEntity extends CommonEntity {
  @OneToMany((type) => VotedUsersEntity, (voted) => voted.vote, {})
  voted: VotedUsersEntity[];
}

// users.entity.ts
@Entity({ name: 'users' })
export class UsersEntity extends CommonEntity {

  @OneToMany((type) => VotedUsersEntity, (votedUsers) => votedUsers.user, {
    onDelete: 'CASCADE',
  })
  votedUsers?: VotedUsersEntity[];
}

 

객체 생성은 다음과 같이 작성했습니다. 투표 기능 로직입니다.

 

// votes.service.ts

  async choiceVote( voteId: number, userId: number ) {
    const user = await this.usersService.findUserByWhereOption({
      id: userId,
    });
    const vote = await this.getVoteById(voteId);

    const voted = new VotedUsersEntity();
    voted.vote = vote;
    voted.user = user;
    await this.votedRepository.save(voted);    
    ...
  }