NestJS TypeORM

Peter·2020년 10월 22일
9

nestjs

목록 보기
8/11
post-thumbnail

1. 패키지 설치 및 기본 모듈 설정

1) 패키지 설치

> yarn add @nestjs/typeorm typeorm mysql       

2) 기본 모듈 설정

ormconfig.json

{
  "type": "mysql",
  "host": "localhost",
  "port": 3308,
  "username": "peter",
  "password": "1234",
  "database": "typeorm_test_db",
  "entities": ["dist/**/*.entity{.ts,.js}"],
  "synchronize": true
}

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forRoot()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

프로젝트 root 폴더에 ormconfig.json 파일을 작성하고, 위와 같이 app.module.ts 파일에서 TypeOrmModule.forRoot() 로 import 하면 typeorm을 사용 가능합니다.
(database 는 ormconfig.json 에 기록한 내용과 동일하게 셋팅했다고 가정합니다.)

2. Entity CRUD

1) Entity 추가

user.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

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

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}

위와 같이 User class 를 작성한 뒤 프로젝트를 실행하면 mysql database에 user 테이블이 자동으로 생성됩니다.
(ormconfig.json 파일의 "synchronize" 옵션이 false이면 자동생성하지 않습니다.)

2) CRUD

user.service.ts

import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { InjectRepository } from '@nestjs/typeorm';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async add(user: User): Promise<void> {
    await this.userRepository.save(user);
  }

  findAll(): Promise<User[]> {
    return this.userRepository.find();
  }

  findOne(id: string): Promise<User> {
    return this.userRepository.findOne(id);
  }

  async modify(user: User): Promise<void> {
    const userNew = await this.userRepository.findOne(user.id);
    userNew.firstName = user.firstName;
    userNew.lastName = user.lastName;
    userNew.isActive = user.isActive;
    await this.userRepository.save(userNew);
  }

  async remove(id: string): Promise<void> {
    await this.userRepository.delete(id);
  }
}

위와 같이 save, findXX, delete 함수로 CRUD를 구현할 수 있습니다.
update는 보통의 find와 save의 조합으로 처리하는 것 같습니다.

3. OneToOne

1) 관계 추가

Entity 간 1:1 관계 예제를 위해 Profile Entity를 추가합니다.

profile.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Profile {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  gender: string;

  @Column()
  address: string;
}

Profile은 User의 상세정보로 User와 1:1 관계를 갖습니다.
User Entity에 관계설정 부분을 추가합니다.

user.entity.ts

import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Profile } from '../profile/profile.entity';

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

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;

  @OneToOne(() => Profile)
  @JoinColumn()
  profile: Profile;
}

위와 같이 작성한 뒤, 프로젝트를 실행하면 profile 테이블이 생성됩니다.
이 때 user 테이블에 profileId가 외래키(profile.id 참조)로써 추가됩니다.

2) Create

user.service.ts

  async add(user: User): Promise<void> {
    const profile = user.profile;
    await this.profileRepository.save(profile); // 저장하면 DB에 profile row가 생성되고, 자동생성된 id가 profile 객체의 id에 매핑됩니다.
    await this.userRepository.save(user);
  }

위와 같이 profile과 user를 각각의 테이블에 저장할 수 있습니다.

3) Read

user.service.ts

  findAll(): Promise<User[]> {
    return this.userRepository.find({relations: ["profile"]});
  }

위와 같이 간단하게 User와 Profile을 Join한 결과를 가져올 수 있습니다.
하지만, 보통의 경우 검색조건과 순서 및 페이징 등이 추가될 수 있습니다.

  findAll(): Promise<User[]> {
    return this.userRepository
      .createQueryBuilder('user')
      .leftJoinAndSelect('user.profile', 'profile')
      .orderBy('user.id', 'DESC')
      .getMany();
  }

위와 같이 구체적인 상황을 서술할 수 있습니다.

  findOne(id: string): Promise<User> {
    return (
      this.userRepository
        .createQueryBuilder('user')
        .leftJoinAndSelect('user.profile', 'profile')
        .where('user.id = :id', { id })
        .getOne()
    );
  }

하나의 User를 조회하고 싶은 경우 위와 같이 작성할 수 있습니다.

4) Update

  async modify(newUser: User): Promise<void> {
    const user = await this.findOne(newUser.id.toString());
    user.firstName = newUser.firstName;
    user.lastName = newUser.lastName;
    user.isActive = newUser.isActive;

    const profile = user.profile;
    profile.address = newUser.profile.address;
    profile.gender = newUser.profile.gender;

    await this.userRepository.save(user);
    await this.profileRepository.save(profile);
  }

3) READ에서 작성한 findOne 함수를 재활용하여 위와 같이 Update를 구현할 수 있습니다.

5) Delete

  async remove(id: string): Promise<void> {
    const user = await this.findOne(id);
    const profile = user.profile;
    await this.userRepository.delete(id);
    await this.profileRepository.delete(profile.id);
  }

위와 같이 작성하면 id를 key로 갖는 user가 삭제되고, user와 매핑된 profile도 삭제됩니다.
이 때, user.profileId가 외래키이기 때문에 profile을 먼저 지울 수 없어서 user를 먼저 삭제합니다. (둘의 순서를 바꾸면 에러가 나면서 동작하지 않습니다.)

4. OneToMany

1) 관계 추가

한 명의 사용자(User)가 여러 개의 글(Posts)을 쓸 수 있습니다.
이 경우, User 와 Posts 는 1:N (One To Many)관계에 있습니다.

Posts

import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { User } from '../user/user.entity';

@Entity()
export class Posts {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  contents: string;

  @ManyToOne(() => User, user => user.posts)
  user: User;
}

User

import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Profile } from '../profile/profile.entity';
import { Posts } from '../post/post.entity';

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

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;

  @OneToOne(() => Profile, { cascade: true })
  @JoinColumn()
  profile: Profile;

  @OneToMany(() => Posts, post => post.user)
  posts: Posts[];
}

위와 같이 작성하면, DB에 posts 테이블이 생성됩니다.
이 때, posts 테이블에 userId 가 외래키로 추가됩니다. (user.id 를 참조합니다.)

2) Create

특정 User가 올린 Posts를 저장하기 위해서는 userId와 Posts 내용이 필요합니다.

PostReqDto

export class PostReqDto {
  userId: number;
  title: string;
  contents: string;
}

PostController

import { Body, Controller, Logger, Post } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { PostService } from './post.service';
import { PostReqDto } from './post.req.dto';

@Controller('post')
export class PostController {
  constructor(private readonly userService: UserService, private readonly postService: PostService) {}

  @Post()
  async add(@Body() req: PostReqDto): Promise<void> {
    const user = await this.userService.findOne(req.userId.toString());
    await this.postService.add(user, req);
  }
}

컨트롤러에서는 요청 파라미터에 있는 userId로부터 앞서 작성했던 UserService.findOne() 메소드를 이용하여 user 객체를 조회해옵니다.
user 객체는 Posts와 User 간의 관계를 정의해주기 위해서 필요합니다.

PostService

import { Injectable } from '@nestjs/common';
import { User } from '../user/user.entity';
import { PostReqDto } from './post.req.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Posts } from './post.entity';
import { Repository } from 'typeorm';

@Injectable()
export class PostService {
  constructor(@InjectRepository(Posts) private postsRepository: Repository<Posts>) {}

  async add(user: User, req: PostReqDto) {
    const posts = new Posts();
    posts.contents = req.contents;
    posts.title = req.title;
    posts.user = user;
    await this.postsRepository.save(posts);
  }
}

위와 같이 posts 객체를 생성하고 프로퍼티를 셋팅한 뒤 PostRepository를 이용하여 저장하면 해당 user가 작성한 posts 가 저장됩니다.
아래는 userId = 1로 셋팅하여 테스트해본 결과입니다.

위와 같이 posts.user 를 셋팅하는 방법도 있지만, "user.posts = [posts]"와 같이 셋팅한 뒤 userRepository에서 user를 save하는 방법도 있습니다.
자세한 내용은 typeorm 공식문서를 참고합시다!
https://typeorm.io/#/many-to-one-one-to-many-relations

3) Read

다건 1.

await this.postsRepository.find();
...

// return 형태
   [ 
    {
        "id": 1,
        "title": "글 제목",
        "contents": "글 내용"
    },
    ...
   ]

위와 같이 find()를 실행하면 post에 대한 정보만 조회됩니다.(연관된 user에 대한 정보는 조회되지 않습니다.)

다건 2.

return await this.postsRepository.find({relations: ["user"]});

// return 형태
    {
        "id": 1,
        "title": "글 제목",
        "contents": "글 내용",
        "user": {
            "id": 1,
            "firstName": "Peter",
            "lastName": "Choi",
            "isActive": true
        }
    },

연관된 user에 대한 정보까지 조회하기 위해서는 위와 같이 "relations" 옵션을 추가하면 됩니다.

다건 3.

return await this.postsRepository
      .createQueryBuilder('posts')
      .leftJoinAndSelect('posts.user', 'user')
      .orderBy('posts.id', 'DESC')
      .getMany();

대부분 게시글은 최신글 순으로 보여주기 때문에 위와 같이 order by 조건을 추가해줘야 합니다.
그 이외에도 검색 조건 등을 적용하기 위해 where 절이 필요하게 되는게, 이런 여러가지 조건들을 처리해주기 위해서는 위와 같이 QueryBuilder로 query 옵션을 추가할 수 있습니다.

단건

return await this.postsRepository
      .createQueryBuilder('posts')
      .leftJoinAndSelect('posts.user', 'user')
      .where('posts.id = :id', { id })
      .getOne();

단건 조회는 위와 같이 where 절에 posts.id 일치 조건을 추가합니다.

5. ManyToMany

TBW...

2개의 댓글

comment-user-thumbnail
2021년 3월 15일

찾고있던 내용인데, 감사합니다~

답글 달기
comment-user-thumbnail
2021년 6월 26일

2) Create 의 경우에는 cascading 을 사용하면 좋을 것 같군요!

답글 달기