개발일지
[NestJS] TypeORM 적용하기 본문
- 기존 프로젝트에 적용되었던 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);
...
}
'NestJS, Node.js > #01 Project - 투표 커뮤니티' 카테고리의 다른 글
[NestJS] DI, DIP에 대해 알아보고 구현해보기 (0) | 2023.03.16 |
---|---|
[NestJS] Redis로 JWT Blacklist 적용하기 (0) | 2023.03.13 |
[NestJS] S3로 이미지 파일 관리하기 (0) | 2022.12.09 |
[NestJS] ValidationPipe 커스텀하기 (0) | 2022.11.30 |
[NestJS] Prisma 환경 변수 문제 해결하기 (0) | 2022.11.16 |