TypeORM with NestJS
TypeORM 사용해보기
TypeORM으로 데이터베이스 연결하기
NestJS 실습용 코드 - 회원가입 서버 구현 중…
...
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
...
TypeOrmModule.forRoot({ // 1
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'test',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'], // 2
synchronize: true, // 3
}),
],
})
export class AppModule {}
- AppModule에 TypeOrmModule을 동적 모듈로 가져온다. (1)
- 소스 코드 내에서 TypeORM이 구동될 때 인식하도록 할 엔티티 클래스의 경로를 지정한다. (2)
- synchronize 옵션은 서비스 구동 시 소스 코드 기반으로 데이터베이스 스키마를 동기화할지 여부이다. (3)
- 로컬 환경에서 구동시 개발의 편의를 위해 true로 한다.
다음은, TypeOrmModule.forRoot 함수에 전달하는 TypeOrmModuleOptions 객체이다.
export declare type TypeOrmModuleOptions = {
retryAttempts?: number; // 연결시 재시도 회수 (default: 10)
retryDelay?: number; // 재시도 간의 지연시간 (단위: ms, default: 3000)
toRetry?: (err: any) => boolean; // 에러가 났을 때 연결을 시도할지 판단하는 함수
autoLoadEntities?: boolean; // 엔티티를 자동 로드할지 여부
keepConnectionAlive?: boolean; // 애플리케이션 종료 후 연결을 유지할 지 여부
verboseRetryLog?: boolean; // 연결 재시도시, verbose 에러메시지를 보여줄지 여부
} & Partial<ConnectionOptions>;
ormconfig.json 활용하기
Nest는 데이터베이스를 연결하는 또 다른 방법을 제공한다. 루트 디렉토리에
ormconfig.json
파일이 있다면TypeOrmModule.forRoot()
에 옵션 객체를 전달하지 않아도 된다.
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "test",
"database": "test",
"entities": ["dist/**/*.entity{.ts,.js}"],
"synchronize": true
}
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot(),
],
})
export class AppModule {}
☑️ ormconfig.json으로 기술된 데이터베이스 옵션에는 비밀번호 같은 민감한 정보가 포함되어 있다.
☑️ 그런데 JSON은 dotenv로 읽어온 값을 넣을 수 없기 때문에 개발환경에 따라 ormconfig.json파일을 프로비저닝할 때 환경에 맞는 파일로 교체해 주는 장치가 필요하다.
📌 이 부분은 다른 페이지에 따로 정리하겠다.
요청한 정보 데이터베이스에 저장하기
위에서 언급했듯이 회원가입 관련 서버를 실습 중에 typeORM을 진행중이다. 따라서, 회원 가입을 요청한 유저의 정보를 저장해보려 한다.
import { Column, Entity, PrimaryColumn } from "typeorm";
@Entity("User")
export class UserEntity {
@PrimaryColumn()
id: string;
@Column({ length: 30 })
name: string;
@Column({ length: 60 })
email: string;
@Column({ length: 30 })
password: string;
@Column({ length: 60 })
signupVerifyToken: string;
}
☑️ 위의 유저 엔터티를 데이터베이스에서 사용할 수 있도록 TypeOrmModuleOptions의 entities 속성의 값으로 넣어주면 된다. (→ 이미 위에서 구현했다.)
{
...
"entities": ["dist/**/*.entity{.ts,.js}"],
...
}
이후 서버를 실행시키면 다음과 같이 User 테이블이 생성됐다.
...
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './entity/user.entity';
@Module({
imports: [
...
TypeOrmModule.forFeature([UserEntity]),
],
...
})
export class UsersModule {}
☑️ UsersModule에서 forFeature() 메소드로 유저 모듈 내에서 사용할 저장소를 등록한다.
...
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserEntity } from './entity/user.entity';
export class UsersService {
constructor(
...
@InjectRepository(UserEntity) private usersRepository: Repository<UserEntity>,
) { }
...
}
☑️ UsersService에서 @InjectRepository()
데코레이터로 유저 레포지토리를 주입한다.
☑️ 여기서 주입한 레포지터리를 통해 다음과 같이 데이터베이스에 저장한다.
...
import { ulid } from "ulid";
...
private async saveUser(name: string, email: string, password: string, signupVerifyToken: string) {
const user = new UserEntity();
user.id = ulid();
user.name = name;
user.email = email;
user.password = password;
user.signupVerifyToken = signupVerifyToken;
await this.usersRepository.save(user);
}
...
☑️ 다음은 기존 유저 정보를 확인하는 함수이다.
☑️ 가입시 이미 존재하는 회원이면 422 에러를 내도록 한다. (UnprocessableEntityException
)
async createUser(name: string, email: string, password: string) {
const userExist = await this.checkUserExists(email);
if (userExist) {
throw new UnprocessableEntityException("해당 이메일로는 가입할 수 없습니다."); // 422 error
}
...
}
...
private async checkUserExists(emailAddress: string): Promise<boolean> {
const user = await this.userRepository.findOne({
where: {
email: emailAddress,
},
});
return user !== undefined;
}
트랜잭션 처리
📌 TypeORM에서 트랜잭션을 사용하는 방법은 아래와 같이 3가지가 있다.
-
QueryRunner
를 이용하여 단일 DB 커넥션 상태를 생성하고 관리하기 -
transaction
객체를 생성해서 이용하기 -
@Transaction
,@TransactionManager
,@TransactionRepository
데코레이터를 사용하기.
⚠️ 이중 데코레이터를 사용하는 방식은 Nest에서 권장하지 않는다. 마지막 방법은 제외하자.
QueryRunner 클래스를 사용하는 방법
QueryRunner를 사용하면 트랜잭션을 완전히 제어할 수 있다.
...
import { Connection, ... } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
...
private connection: Connection,
) { }
...
}
☑️ 먼저 typeorm에서 제공하는 Connection 객체를 주입했다.
👩🏻💻 참고 문서대로 하던 중 다음과 같은 에러를 만났다.
👉 구글링을 해보니, 0.3.0 버전 업데이트 당시 Connection이 DataSource라는 이름으로 변경됐다고 나온다.(지금 사용중인 typeorm version: ^0.3.7)
)
👉 복잡할 건 없다. 위 코드를 다음과 같이 수정해주기만 하면 된다. (변수명은 고칠 필요는 없는데 가독성을 위해…)
...
import { DataSource, ... } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
...
private dataSource: DataSource,
) { }
...
}
☑️ DataSource 객체에서 트랜잭션 생성이 가능하다.
☑️ 유저를 저장하는 로직에 트랜잭션을 걸어보자. (위에서 구성해준 saveUser를 지우고 대신 saveUserUsingQueryRunner을 구성해보자)
private async saveUserUsingQueryRunner(
name: string,
email: string,
password: string,
signupVerifyToken: string
) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction(); // 트랜잭션 시작
try {
const user = new UserEntity();
user.id = ulid();
user.name = name;
user.email = email;
user.password = password;
user.signupVerifyToken = signupVerifyToken;
await queryRunner.manager.save(user);
await queryRunner.commitTransaction(); // 에러 x -> commit
} catch (e) {
await queryRunner.rollbackTransaction(); // 에러 o -> rollback
} finally {
await queryRunner.release(); // QueryRunner 해제
}
}
transaction 객체를 생성해서 사용하는 방법
💡 또 다른 방법으로 dataSource 객체 내의 transaction 메서드를 바로 이용하는 방법도 있다.
👉 해당 메서드의 주석을 보면 다음과 같다.
transaction 메소드는 주어진 함수 실행을 트랜잭션으로 래핑한다.
모든 데이터베이스 연산은 제공된 엔티티 매니저를 이용하여 실행해야 한다.
☑️ transaction 메서드는 EntityManager
를 콜백으로 받아 사용자가 어떤 작업을 수행할 함수를 작성할 수 있도록 해준다.
private async saveUserUsingTransaction(
name: string,
email: string,
password: string,
signupVerifyToken: string
) {
await this.dataSource.transaction(async (manager) => {
const user = new UserEntity();
user.id = ulid();
user.name = name;
user.email = email;
user.password = password;
user.signupVerifyToken = signupVerifyToken;
await manager.save(user);
});
}