17. typeORM 및 DB관련 기본 개념

유현준·2022년 9월 4일
1

hello! Nest

목록 보기
17/17

0. 유의사항

  • 라이브러리에 대한 설명을 구체적으로 모두 다 작성하는데는 한계가 있다. 필자 또한 이 블로그를 typeORM을 사용하면서 참고하기 위한 목적으로 쓰지만, 기본적으로 공식문서를 참고하는 것이 가장 정확하다. 이에, 구체적인 내용은 공식문서를 참고하는 것이 좋으며, 필자도 아래 내용 중에 앞으로 공식문서를 통해 새로 배우는 내용은 수정/보충하면서 학습할 예정이다.
  • CRUD와 관련된 QUERY문은 별도로 기록하지 않았다. 참고문헌과 아래 참고자료의 강의 코드를 참고하는 것이 더 효율적이라 판단했기 때문이다.

1. 참고자료

1. typeORM

  • nest.js 환경에서 가장 많이 사용되는 ORM 라이브러리다.
  • 익히 듣기로, typeORM은 특히 migration에 강점을 가지고 있다고 한다.

2.준비물

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

3. db 옵션 설정 및 연결

  • app.module에서 사용할 db와 옵션을 다음과 같이 설정해줄 수 있다.
  • 아래 내용은 configmodule과 연동하여, 환경변수를 활용해서 typeORM 기본 설정을 세팅하는 것이다.

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],
})

4. entity 생성

  • 먼저, DB와 관련해서 entity라는 것은 물리적 개념의 테이블을 논리적 개념으로 이야기하는 것을 의미한다. 추측컨대, 테이블 === 엔티티 === 도메인인 것 같다.

  • typeORM을 통한 entity 생성은 두 가지 스타일이 있다고 한다. 이에 대해서는 추후에 좀 더 공부를 하면서 그 차이점을 비교해볼 것이다.

    1) Active Record 패턴
    => 레포지토리 레이어를 두지 않고 엔터티 자체에서 직접 접근하여 로직을 수행합니다. 비교적 작은 서비스에 어울리는 패턴입니다.
    모든 엔터티들은 TypeORM에서 제공하는 BaseEntity를 상속하고 이 BaseEntity에는 대부분의 기본 레포지토리에서 제공하는 메서드들이 담겨있습니다.
    2) Data Mapper 패턴
    => 엔터티에 직접 접근하는 방식이 아닌 레포지토리 레이어를 두고 접근합니다

  • data mapper 패턴에 따른 entity의 생성은 다음과 같은 구조에 따른다.

    1. 기능과 무관하게 테이블에 공통적으로 포함되는 내용은 commonEntity로 작성한다.
    2. 기능별 entity들은 commonEntity를 상속하여 작성한다.
    3. 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[]
}
  • commonEntity에서 deleatedAt이 있는 이유는 유지보수 차원에서 soft delete를 구현하기 위함이라고 한다.
  • BlogEntity에는 tagEntity와 다대다 관계, userEntity와 1대다 관계가 맺어져 있는데, 이와 관련된 코드는 공식문서와 강의코드를 참고하는 것이 훨씬 낫다.
profile
차가운에스프레소의 개발블로그입니다. (22.03. ~ 22.12.)

0개의 댓글