TypeOrm

히반·2023년 4월 4일
0
post-thumbnail

Docker Volume 생성

실제로 DB는 도커 컨테이너로 잘 사용하지 않는다고 합니다.
일반적으로는 RDS나 온프레미스 서버에 올려서 사용한다고 합니다.

docker volume create [볼륨 이름]

Docker기반 MariaDB 컨테이너 구동

#docker 최신 버전 image pull
docker pull mariadb
#docker container를 실행하는데 
# --name -> container 이름을 mariadb로 하고
# -d -> detach 모드로 실행
# -p 3306:3306 -> docker port 3306과 local port 3306을 연결
# --restart=always -> 도커가 실행되면 컨테이너도 실행
# -v -> [도커 볼륨 이름]:[볼륨과 연결할 컨테이너 내부 경로]
# -e -> 추가 환경 설정
docker run --name mariadb -d -p 3306:3306 --restart=always -v [볼륨이름]:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=[루트 비밀번호] [이미지 이름]

TypeOrm설치

yarn add @nestjs/typeorm typeorm mysql2

Config 파일 생성 및 연결

폴더 구조

synchronize : 설정 시 table이 없을 시 자동생성 합니다. 추가적으로 변경사항이 있을시 데이터가 지워질 수 있으니 운영에서는 절대 true로 놓으면 안됩니다..

autoLoadEntities : 설정 시 entity를 자동으로 찾아서 등록합니다.

logging 설정 시 typeorm에서 자동으로 생성되는 sql문 로그를 볼 수 있습니다.

//typeorm.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export const typeORMConfig: TypeOrmModuleOptions = {
  type: 'mariadb',
  host: 'localhost',
  port: 3306,
  username: 'root',
  password: '0000',
  database: 'database-name',
  synchronize: true,
  autoLoadEntities: true,
	logging:false
};
//app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeORMConfig } from 'config/typeorm.config';

@Module({
  imports: [TypeOrmModule.forRoot(typeORMConfig)],
})
export class AppModule {}

Entity생성

@PrimaryGeneratedColumn은 typeorm에서 자동으로 만들어주는 프라이머리 키입니다.

‘increament’는 숫자 1,2,3 … 순서로 진행하고 ‘uuid’로 설정하면 uuid v4로 생성됩니다.

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

@Entity()
export class User {
  @PrimaryGeneratedColumn('increment')
  _id: number;

  @Column()
  name: string;

  @Column()
  age: number;
}

Repository 연결 및 CRUD

TypeOrmModule.forFeature([example])의 인자로 해당 모듈이 사용하는 모든 엔티티를 입력해 주어야 합니다.

종속관계에 있는 모든 엔티티를 입력해 주어야 해서 헷갈릴 수 있습니다.

//user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User } from './entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}
//user.service.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    const result = await this.userService.create(createUserDto);
    return result;
  }

  @Get()
  async findAll() {
    const result = await this.userService.findAll();
    return result;
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    const result = await this.userService.findOne(+id);
    return result;
  }

  @Patch(':id')
  async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    const result = await this.userService.update(+id, updateUserDto);
    return result;
  }

  @Delete(':id')
  async delete(@Param('id') id: string) {
    const result = await this.userService.delete(+id);
    return result;
  }
}

Transaction

한번에 처리되어야 하는 로직의 모음을 트랜잭션이라고 합니다.

예를 들어 결제, 이체 같은 경우나 데이터를 생성하거나 수정 혹은 삭제하는 부분에서 사용합니다.

에러가 났음에도 불구하고 DB에 반영되면 결과와 내부 데이터가 달라질 수 있기 때문입니다.

//user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';

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

  async create(createUserDto: CreateUserDto) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      const entity = this.userRepository.create(createUserDto);
      const result = await queryRunner.manager.getRepository(User).save(entity);
      await queryRunner.commitTransaction();
      return result;
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();
    }
  }

  async findAll() {
    const result = await this.userRepository.find();
    return result;
  }

  async findOne(id: number) {
    const result = await this.userRepository.findOneBy({ _id: id });
    return result;
  }

  async update(id: number, updateUserDto: UpdateUserDto) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      const result = await queryRunner.manager
        .getRepository(User)
        .update({ _id: id }, updateUserDto);
      await queryRunner.commitTransaction();
      return result;
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();
    }
  }

  async delete(id: number) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      const result = await queryRunner.manager
        .getRepository(User)
        .delete({ _id: id });
      await queryRunner.commitTransaction();
      return result;
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();
    }
  }
}

Relations(ManyToOne, OneToMany, OneToOne)

ManyToMany도 존재하지만 실무에서는 사용하지 않는다고 합니다.

중간에 MapperTable(JoinTable)을 직접 만들어서 Join해서 사용하는 것을 추천합니다.

ManyToOne과 OneToMany의 경우 한 엔티티가 다른 엔티티를 여러개 가지는 경우와 그 반대의 경우가 됩니다.

User와 Photo로 예를 들자면 한 사람은 여러장의 사진을 가질 수 있고 여러장의 사진은 한 사람의 것 일 수 있습니다.

이 경우 User 엔티티에 OneToMany로 Photo 배열이 들어가고 Photo 엔티티에는 ManyToOne로

User가 들어가면 됩니다.

폴더 구조

//hobby.entity.ts
import { HobbyMapper } from 'src/hobby/entities/hobbyMapper.entity';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Hobby {
  @PrimaryGeneratedColumn('increment')
  _id: number;

  @Column()
  name: string;

  @OneToMany(() => HobbyMapper, (hobbyMapper) => hobbyMapper.hobby)
  hobbyMappers: HobbyMapper[];
}
//hobbyMapper.entity.ts
import { Hobby } from 'src/hobby/entities/hobby.entity';
import { User } from 'src/user/entities/user.entity';
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';

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

  @Column()
  userId: number;

  @Column()
  hobbyId: number;

  @ManyToOne(() => User, (user) => user.hobbyMappers, { nullable: true })
  @JoinColumn({ name: 'userId' })
  user: User;

  @ManyToOne(() => Hobby, (hobby) => hobby.hobbyMappers, { nullable: true })
  @JoinColumn({ name: 'hobbyId' })
  hobby: Hobby;
}
//job.entity.ts
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { User } from '../../user/entities/user.entity';

@Entity()
export class Job {
  @PrimaryGeneratedColumn('increment')
  _id: number;

  @Column()
  name: string;

  @OneToMany(() => User, (user) => user.job)
  users: User[];
}
//user.entity.ts
import { HobbyMapper } from 'src/hobby/entities/hobbyMapper.entity';
import {
  Column,
  Entity,
  JoinColumn,
  OneToMany,
  PrimaryGeneratedColumn,
  ManyToOne,
  OneToOne,
} from 'typeorm';
import { Job } from '../../job/entities/job.entity';
import { PersonalID } from 'src/user/entities/personal-id.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn('increment')
  _id: number;

  @Column()
  name: string;

  @Column()
  age: number;

  @ManyToOne(() => Job, (job) => job.users)
  @JoinColumn({ name: 'job_id', referencedColumnName: '_id' })
  job: Job;

  @OneToMany(() => HobbyMapper, (hobbyMapper) => hobbyMapper.user)
  hobbyMappers: HobbyMapper[];

  @OneToOne(() => PersonalID, (personalID) => personalID.user)
  @JoinColumn()
  personalID: PersonalID;
}
import { User } from 'src/user/entities/user.entity';
import { Column, Entity, PrimaryGeneratedColumn, OneToOne } from 'typeorm';

@Entity()
export class PersonalID {
  @PrimaryGeneratedColumn('increment')
  _id: number;

  @Column()
  idNumber: string;

  @OneToOne(() => User, (user) => user.personalID)
  user: User;
}

QueryBuilder를 이용한 쿼리(Join)

보통 조인을 한다거나 서브쿼리를 이용한다거나 할 때 이용하는 메소드입니다.

로우쿼리보단 추상화 되어있고 ORM 보다는 덜 추상화 되어있는 형태입니다.

//user.service.ts -> userJoin method
async userJoin(id: number) {
    const result = await this.userRepository
      .createQueryBuilder('user')
      .leftJoinAndSelect('user.hobbyMappers', 'hobbyMapper')
      .leftJoinAndSelect('user.personalID', 'personalID')
      .leftJoinAndSelect('hobbyMapper.hobby', 'hobby')
      .leftJoinAndSelect('user.job', 'job')
      .where({ _id: id })
      .getOne();
    return result;
  }

STI

SingleTableInheritance의 약자로 하나의 테이블을 이용하여 상속 관계를 구현하는 기법입니다.

MariaDB는 지원하는데 다른 DB는 잘 모르겠습니다.

아래의 예시를 보면 Player를 부모로 가지는 Footballer과 Cricketer를 만들었습니다.

이렇게 작성하고 부모 repository에 save하기 전 원하는 자식 repository의 create메소드를 사용하여

entity 만들어 playerRepository에 넣게되면 자식entity로 만들어져 들어갑니다.

이를 확인하기 위해서 부모 repository에서 자식 entity로 저장했던 entity를 가져와서 오버라이딩된

play 메소드를 실행해보면 자식 entity의 play가 실행될 것입니다.

STI를 사용할 경우 자식 entity들이 가지는 컬럼들을 전부 테이블에 들어가게되는데 만약 그 컬럼을 가지지 않는

자식 entity의 경우에는 해당 컬럼에 NULL이 들어갑니다.

이 경우 공간 낭비를 걱정할 수 있는데 MariaDB의 경우 NULL 값을 가지는 공간의 경우 고정길이를 사용할 경우

크기를 가지지만 가변길이를 사용 할 경우 nullable 체크 1bit 이외에는 따로 할당을 하지 않아

낭비 되지 않는다고 합니다.

//player.entity.ts
import {
  ChildEntity,
  Column,
  Entity,
  PrimaryGeneratedColumn,
  TableInheritance,
} from 'typeorm';

@Entity({ name: 'Player' })
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
export class Player {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  public play() {
    console.log(`Player [${this.name}]: Play!!`);
  }
}

@ChildEntity('Footballer')
export class Footballer extends Player {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  club: string;

  public play(): void {
    console.log(`Footballer [${this.name}]: Play!!`);
  }
}

@ChildEntity('Cricketer')
export class Cricketer extends Player {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  batting: string;

  public play(): void {
    console.log(`Cricketer [${this.name}]: Play!!`);
  }
}
//player.module.ts
import { Module } from '@nestjs/common';
import { PlayerService } from './player.service';
import { PlayerController } from './player.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cricketer, Footballer, Player } from './entities/player.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Player, Footballer, Cricketer])],
  controllers: [PlayerController],
  providers: [PlayerService],
})
export class PlayerModule {}
//player.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { PlayerService } from './player.service';
import { CreatePlayerDto } from './dto/create-player.dto';

@Controller('player')
export class PlayerController {
  constructor(private readonly playerService: PlayerService) {}
  @Post('/:entityName')
  async createPlayer(
    @Param('entityName') entityName: string,
    @Body() createPlayerDto: CreatePlayerDto,
  ) {
    return await this.playerService.createPlayer(entityName, createPlayerDto);
  }

  @Get()
  async findAll() {
    return await this.playerService.findAll();
  }
}
//player.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreatePlayerDto } from './dto/create-player.dto';
import { UpdatePlayerDto } from './dto/update-player.dto';
import { Cricketer, Footballer, Player } from './entities/player.entity';

@Injectable()
export class PlayerService {
  constructor(
    @InjectRepository(Player)
    private readonly playerRepository: Repository<Player>,
    @InjectRepository(Footballer)
    private readonly footballerRepository: Repository<Footballer>,
    @InjectRepository(Cricketer)
    private readonly cricketerRepository: Repository<Cricketer>,
  ) {}
  async createPlayer(entityName: string, createPlayerDto: CreatePlayerDto) {
    const entity = await this.createEntity(entityName, createPlayerDto);
    const result = await this.playerRepository.save(entity);
    return result;
  }

  async findAll() {
    const result = await this.playerRepository.find();
    for (const iterator of result) {
      iterator.play();
    }
    return result;
  }

  async createEntity(entityName: string, createPlayerDto: CreatePlayerDto) {
    let entity = null;

    switch (entityName) {
      case 'player':
        entity = this.playerRepository.create(createPlayerDto);
        break;
      case 'footballer':
        entity = this.footballerRepository.create(createPlayerDto);
        break;
      case 'cricketer':
        entity = this.cricketerRepository.create(createPlayerDto);
        break;
    }

    return entity;
  }
}

참고

0개의 댓글

관련 채용 정보