티스토리 뷰

TypeORM이란?

■ NodeJS와 같은 환경에서 실행할수 있는 ORM이며 TypeScript및 JavaScript와 함께 사용할수 있다.

■ 항상 최신 JavaScript기능을 지원하고 몇 개의 테이블이 있는 작은 응용 프로그램에서 여러 데이터베이스가 있는 

    대규모 엔터프라이즈 응용 프로그램에 이르기 까지 데이터베이스를 사용하는 모든 종류의 응용프로그램을 개발하는데 

    도움이 되는 추가 기능을 제공한다.

■ 현재 존재하는 다른 모든 JavaScript ORM 과 달리 Active Record 및 Data Mapper패턴을 모두 지원한다.

■ 고품질의 느슨하게 결합된 확장 가능하고 유지 관리 가능한 애플리케이션을 가장 생산적인 방식으로 작성 가능.

dataMapper

• 유지관리에서 도움이 되며, 이는 대규모 앱에서 더 효과적.

• 저장소 라는 별도의 클래스에서 모든 쿼리 메서드를 정의하고 저장소를 사용하여 개체를 저장, 제거 및 로드가능.

• 모델 대신 저장소 내의 데이터베이스에 액세스 하는 접근 방식

 

모델

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    firstName: string

    @Column()
    lastName: string

    @Column()
    isActive: boolean
}

 

저장소

const userRepository = dataSource.getRepository(User)

// 데이터 저장
const user = new User()
user.firstName = "Timber"
user.lastName = "Saw"
user.isActive = true
await userRepository.save(user)

// 데이터 삭제
await userRepository.remove(user)

// 데이터 읽기
const users = await userRepository.find({ skip: 2, take: 5 })
const newUsers = await userRepository.findBy({ isActive: true })
const timber = await userRepository.findOneBy({
    firstName: "Timber",
    lastName: "Saw",
})
Active Record

작업을 단순하게 유지하는 데 도움이 되며 작은 앱에서도 잘 작동함.

단순하므로 항상 더 나은 유지관리를 할수가 있음

• 모델 자체 내에서 모든 쿼리 메서드를 정의하고 모델 메서드를 사용하여 객체를 저장, 제거 및 로드 가능

• 모델 내에서 데이터베이스에 액세스하는 접근방식

 

모델

import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class User extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    firstName: string

    @Column()
    lastName: string

    @Column()
    isActive: boolean

    static findByName(firstName: string, lastName: string) {
        return this.createQueryBuilder("user")
            .where("user.firstName = :firstName", { firstName })
            .andWhere("user.lastName = :lastName", { lastName })
            .getMany()
    }
}

 

사용

const timber = await User.findByName("Timber", "Saw")

 

NestJS에서 사용하기

1. 루트 디렉토리로 이동한다음 패키지 설치하기

npm i @nestjs/typeorm typeorm mysql2
// Node.js에서 .env역할을 하는 Nest.js의 @nestjs/config 패키지 설치
// .env의 스키마(형식)를 강제할 수 있는 joi패키지설치
npm i @nestjs/config joi

 

2. app.module.ts

import Joi from 'joi';

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Post } from './post/entities/post.entity';
import { PostModule } from './post/post.module';


const typeOrmModuleOptions = {
  useFactory: async (
    configService: ConfigService,
  ): Promise<TypeOrmModuleOptions> => ({
    type: 'mysql',
    host: configService.get('DB_HOST'),
    port: configService.get('DB_PORT'),
    username: configService.get('DB_USERNAME'),
    password: configService.get('DB_PASSWORD'),
    database: configService.get('DB_NAME'),
    entities: [Post],
    synchronize: configService.get('DB_SYNC'),
    logging: true,
  }),
  inject: [ConfigService],
};

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        DB_HOST: Joi.string().required(),
        DB_PORT: Joi.number().required(),
        DB_USERNAME: Joi.string().required(),
        DB_PASSWORD: Joi.string().required(),
        DB_NAME: Joi.string().required(),
        DB_SYNC: Joi.boolean().required(),
      }),
    }),
    TypeOrmModule.forRootAsync(typeOrmModuleOptions),
    PostModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

• typeOrmModuleOptions

    - TypeORM 모듈의 옵션을 의미

    - TypeORM 설정을 하드 코딩하지 않고 동적으로 .env파일에서 설정을 불러와 주입하기 위한 방법.

    - useFactory

       ° TypeOrmModule.forRootAsync()함수에서 설정 객체를 동적으로 생성하기 위해 사용.

    - inject: [ConfigService]

       ° useFactory에서 사용할 ConfigService를 DI하는것.

 

• ConfigModule

    - isGlobal : true

       ° 전역적으로 사용함을 의미

    - validationSchema

       ° 여기에서 정의한 스키마대로 .env가 정의되어있지 않으면 서버는 실행하지 않는다.

          → DB_NAME같은 경우 값이 누락되어 있거나 해당 값이 문자열이 아닌 다른 값이라면 실행 X

 

• TypeOrmModule.forRootAsync

    - 동적으로 TypeORM 설정을 주입할 때 사용

    - 환경 변수나 다른 설정 서비스를 통해 데이터베이스 연결 정보를 가져올 때 유용

 

3. post.entity.ts

import { IsNumber, IsString } from 'class-validator';
import {
  Column,
  CreateDateColumn,
  DeleteDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity({
  name: 'posts',
})
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @IsString()
  @Column('varchar', { length: 50, nullable: false })
  title: string;

  @IsString()
  @Column('varchar', { length: 1000, nullable: false })
  content: string;

  @IsNumber()
  @Column('int', { select: false, nullable: false })
  password: number;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @DeleteDateColumn()
  deletedAt?: Date;
}

• @Entity

    - @Entity 어토네이션은 해당 클래스가 어떤 테이블에 매핑이 되는지를 나타내는 어노테이션

 

• @PrimaryGeneratedColumn

    - PK에 해당하는 컬럼에 사용하는 어노테이션

 

• @Column, @DateColumn

    - 기본 컬럼과 날짜에 해당하는 컬럼에 사용하는 어노테이션

 

• 비밀번호 컬럼의 select: false설정

    - 일반적인 조회로는 비밀번호를 얻어올 수 없도록하기 위함.

    - select절로 특정하지 않으면 비밀번호는 갖고 올 수 없게 하여 민감한 정보를 최대한 보호

 

• @CreateDateColumn, @UpdateDateColumn, @DeleteDateColumn

    - 레코드가 생성된 날짜 자동기록, 레코드가 수정된 날짜 자동기록, 레코드가 삭제된 날짜 자동기록

    - @DeleteDateColumn은 엔티티가 삭제된 순간 실제로 삭제되는것이아닌 논리적으로 삭제(soft delete)가 되는것

    - 즉, deleteAT ≠ NULL 인 게시물을 가져오게 되면 삭제된것과 같은 효과는 나타낸다.

 

4. post.modules.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { Post } from './entities/post.entity';
import { PostController } from './post.controller';
import { PostService } from './post.service';

@Module({
  imports: [TypeOrmModule.forFeature([Post])],
  controllers: [PostController],
  providers: [PostService],
})
export class PostModule {}

 

• 특정 모듈 서비스에서 사용하고 싶은 리포지토리가 있다면 @Module데코레이터의 import속성에 반드시 넣어줘야

   DI가 원활하게 됨.

 

• TypeOrmModule.forFeature([]) 

    - 특정 엔티티 또는 여러 엔티티를 해당 모듈에 등록하는데 사용.

 

5. post.service.ts

import _ from 'lodash';
import { Repository } from 'typeorm';

import {
  BadRequestException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { RemovePostDTO } from './dto/remove-post.dto';
import { Post } from './entities/post.entity';

@Injectable()
export class PostService {
  private articles: { id: number; title: string; content: string }[] = [];
  private articlePasswords = new Map<number, number>();

  constructor(
    @InjectRepository(Post) private postRepository: Repository<Post>, // 리포지토리를 주입
  ) {}

  async create(createPostDto: CreatePostDto) {
    return (await this.postRepository.save(createPostDto)).id;
    // return하는 이유 :  사용자가 게시글을 작성한 후 해당 게시글의 상세 페이지로 리다이렉션하거나, 추가 작업을 수행하기 위해 생성된 게시글의 ID가 필요할 수 있기 때문
  }

  async findAll() {
    // 간단한 함수이고 반환되는 값의 타입이 명확하거나 자명한 경우에는 반환 타입을 명시하지 않을 수 있다.
    // this.postRepository의 find 함수를 사용하면 특정 조건에 매칭되는 모든 게시물 목록을 Promise 배열로 반환
    // 즉, findAll 함수는 async 함수가 되어야 하며 반환값에는 await
    return await this.postRepository.find({
      where: { deletedAt: null },
      select: ['id', 'title', 'updatedAt'],
    });
  }

  async findOne(id: number) {
    //  id가 number 타입으로 변환할 수 없는 경우에는 컨트롤러에서 전달되는 +id 값이 NaN이 되기 때문에 해당 방어 로직이필요
    if (_.isNaN(id)) {
      throw new BadRequestException('게시물 ID가 잘못되었습니다.');
    }

    return await this.postRepository.findOne({
      where: { id, deletedAt: null },
      select: ['title', 'content', 'updatedAt'],
    });
  }

  async update(id: number, updatePostDto: UpdatePostDto) {
    if (_.isNaN(id)) {
      throw new BadRequestException('게시물 ID가 잘못되었습니다.');
    }

    const { content, password } = updatePostDto;
    // 엔티티코드에서 select:false를 했기 때문에 명시적으로 select를 하지 않으면 password 값을 받아올 수 없다.
    const post = await this.postRepository.findOne({
      select: ['password'],
      where: { id },
    });

    //  import 문에서 참조하는 lodash를 사용하여 _.isNil이라는 함수를 부르고 해당 함수로 article의 NULL 여부를 체크
    if (_.isNil(post)) {
      throw new NotFoundException('게시물을 찾을 수 없습니다.');
    }

    if (!_.isNil(post.password) && post.password !== password) {
      throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
    }

    await this.postRepository.update({ id }, { content }); // 해당하는 id의 content를 수정.
  }

  async remove(id: number, removePostDto: RemovePostDTO) {
    if (_.isNaN(id)) {
      throw new BadRequestException('게시물 ID가 잘못되었습니다.');
    }

    const { password } = removePostDto;

    const post = await this.postRepository.findOne({
      select: ['password'],
      where: { id },
    });

    if (_.isNil(post)) {
      throw new NotFoundException('게시물을 찾을 수 없습니다.');
    }

    if (!_.isNil(post.password) && post.password !== password) {
      throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
    }

    return this.postRepository.softDelete({ id }); // softDelete: DB상에서 지워지는 것이 아니라 deletedAt에 지워진 날짜만 마킹되는 것
  }
}

 

• @InjectRepository()어노테이션을 사용하여 리포지토리를 주입할수 있다.

 

• 리포지토리에는 일반리포지토리와 커스텀 리포지토리가 있다.

 

일반 리포지토리 API

 

• 일반 리포지토리로 데이터베이스 연산이 부족하다면 일반 리포지토리를 상속한 커스텀 리포지토리를 작성하여 사용가능.

 

'프로그래밍 기초 > TypeScript & NestJS' 카테고리의 다른 글

nest개발환경  (0) 2024.03.14
TypeScript 고급 타입  (0) 2024.03.13
TypeScript 기초 다지기  (1) 2024.03.12
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함