1. 필수
@nestjs/typeorm": "^8.0.2", // 필수
"typeorm": "^0.2.38", // 필수
"pg": "^8.7.1", // postgreSQL 사용 시
"reflect-metadata": "^0.1.13",
2. 선택사항
"typeorm-naming-strategies": "^2.0.0",
"typeorm-seeding": "^1.6.1",
// 더미데이터 생성시에 활용한다고 한다.
// 참고: https://velog.io/@alwayslee_12/Typeorm-Seeding
"nestjs-typeorm-paginate": "^3.1.3",
//아마 페이지네이션 구현시 쓰는 라이브러리인것 같다.
// 참고: https://www.npmjs.com/package/nestjs-typeorm-paginate
// 패키지없이 구현방법은 다음을 참고하자. : https://ganzicoder.tistory.com/156
1. app.module.ts
const typeOrmModuleOptions = {
useFactory: async (
configService: ConfigService,
): Promise<TypeOrmModuleOptions> => ({
namingStrategy: new SnakeNamingStrategy(), // camel <-> snake 네이밍을 자동으로 조정해주는 것이다.
type: 'postgres',
host: configService.get('DB_HOST'), // process.env.DB_HOST
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [UserEntity, ProfileEntity, BlogEntity, VisitorEntity, TagEntity],
synchronize: true, //! set 'false' in production = 동기화 여부, 리셋되는 것이므로 prod 레벨에선 해제
autoLoadEntities: true,
logging: true,
keepConnectionAlive: true,
}),
inject: [ConfigService],
}
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 환경변수의 전역 사용 여부
// 환경변수에 대한 validation
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test', 'provision')
.default('development'),
PORT: Joi.number().default(5000),
SECRET_KEY: Joi.string().required(),
ADMIN_USER: Joi.string().required(),
ADMIN_PASSWORD: Joi.string().required(),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().required(),
DB_NAME: Joi.string().required(),
}),
}),
TypeOrmModule.forRootAsync(typeOrmModuleOptions), // typeorm 옵션에 따라 연동
UsersModule,
BlogsModule,
TagsModule,
VisitorsModule,
ProfilesModule,
],
controllers: [AppController],
})
먼저, DB와 관련해서 entity라는 것은 물리적 개념의 테이블
을 논리적 개념으로 이야기하는 것을 의미한다. 추측컨대, 테이블 === 엔티티 === 도메인
인 것 같다.
typeORM을 통한 entity 생성은 두 가지 스타일이 있다고 한다. 이에 대해서는 추후에 좀 더 공부를 하면서 그 차이점을 비교해볼 것이다.
1) Active Record 패턴
=> 레포지토리 레이어를 두지 않고 엔터티 자체에서 직접 접근하여 로직을 수행합니다. 비교적 작은 서비스에 어울리는 패턴입니다.
모든 엔터티들은 TypeORM에서 제공하는 BaseEntity를 상속하고 이 BaseEntity에는 대부분의 기본 레포지토리에서 제공하는 메서드들이 담겨있습니다.
2) Data Mapper 패턴
=> 엔터티에 직접 접근하는 방식이 아닌 레포지토리 레이어를 두고 접근합니다
data mapper 패턴에 따른 entity의 생성은 다음과 같은 구조에 따른다.
- 기능과 무관하게 테이블에 공통적으로 포함되는 내용은 commonEntity로 작성한다.
- 기능별 entity들은 commonEntity를 상속하여 작성한다.
- module은 일반적으로 entity에 따라서 구분하는 것이 바람직하다.
commonEntity와 기능별 entity는 다음과 같을 수 있다.
1. commonEntity
import {
CreateDateColumn,
DeleteDateColumn,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm'
import { IsUUID } from 'class-validator'
import { Exclude } from 'class-transformer'
export abstract class CommonEntity {
@IsUUID()
@PrimaryGeneratedColumn('uuid')
id: string
// 해당 열이 추가된 시각을 자동으로 기록
// 만일 Postgres의 time zone이 'UTC'라면 UTC 기준으로 출력하고 'Asia/Seoul'라면 서울 기준으로 출력한다.
// DB SQL QUERY : set time zone 'Asia/Seoul'; set time zone 'UTC'; show timezone;
@CreateDateColumn({
type: 'timestamptz' /* timestamp with time zone */,
})
createdAt: Date
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date
// Soft Delete : 기존에는 null, 삭제시에 timestamp를 찍는다.
@Exclude()
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null
}
---
2. blogEntity
import { CommonEntity } from '../common/entities/common.entity' // ormconfig.json에서 파싱 가능하도록 상대 경로로 지정
import {
Column,
Entity,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
OneToMany,
} from 'typeorm'
import { VisitorEntity } from '../visitors/visitors.entity'
import { UserEntity } from 'src/users/users.entity'
import { TagEntity } from 'src/tags/tags.entity'
@Entity({
name: 'BLOG',
})
export class BlogEntity extends CommonEntity {
@Column({ type: 'varchar', nullable: false })
title: string
@Column({ type: 'varchar', nullable: true })
description: string
@Column({ type: 'text', nullable: true })
contents: string
//* Relation */
@ManyToOne(() => UserEntity, (author: UserEntity) => author.blogs, {
onDelete: 'CASCADE', // 사용자가 삭제되면 블로그도 삭제된다.(삭제만 cascade 옵션을 줌)
})
@JoinColumn([
// foreignkey 정보들
{
name: 'author_id' /* db에 저장되는 필드 이름 */,
referencedColumnName: 'id' /* USER의 id */,
},
])
author: UserEntity
@ManyToMany(() => TagEntity, (tag: TagEntity) => tag.blogs, {
cascade: true, // 블로그를 통해 태그가 추가, 수정, 삭제되고 블로그를 저장하면 태그도 저장된다.
})
@JoinTable({
// 다대다 관계 시에, 두 엔터티 간 매개 역할을 하는 테이블을 생성
// table
name: 'BLOG_TAG',
joinColumn: {
name: 'blog_id',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'tag_id',
referencedColumnName: 'id',
},
})
tags: TagEntity[]
@OneToMany(() => VisitorEntity, (visitor: VisitorEntity) => visitor.blog, {
cascade: true,
})
visitors: VisitorEntity[]
}
soft delete
를 구현하기 위함이라고 한다.