DB, ORM/🧊 typeORM

Entity(Model) 생성 및 Listener 활용

DarrenKwonDev 2020. 10. 20. 03:57

Entities 정의

 

앞서 DB 구성에서 entities 경로를 다음과 같이 잡아 주었다.

entities: [
  "./entities/**/*.*"
]

 

해당 경로에 entity (Model)을 정의해주도록하자.

 

github.com/typeorm/typeorm#step-by-step-guide

 

typeorm/typeorm

ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Elect...

github.com

 

github 문서의 step-by-step에서는 entity를 생성하는 방식에 대해서 차근차근 알려주고 있다.

@Entity()를 통해 entity를 생성할 수 있고

@Column을 통해 열을 정의할 수 있으며 추가적으로 타입을 지정할 수도 있다.

@PrimaryGeneratedColumn()  @PrimaryColumn()와 같이 key를 지정하는 데코레이션도 있음을 확인할 수 있다.

 

좀 더 자세한 사항은 공식문서에서 확인할 수 있다. 사용하는 DB에 따라 지정할 수 있는 타입을 종류나 @Tree와 같은 새로운 데코레이터도 안내되어 있으니 필요할 때 참고하면 될 것이다.

 

typeorm.io/#/entities/column-types-for-postgres

 

TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server,

 

typeorm.io

 

이를 이용해서 User 항목의 entity를 만들어보자. 조금 verbose한 편인데, 그래도 전반적인 entity를 정의하는데 있어서 필요한 것들을 살펴볼 수 있긴 하다.

 

추가적으로 알아둬야 할 사항은

typescript 기반 객체 지향 프로그래밍의 국룰인 class-validator를 이용하였고 (typeORM과 관련 없지만)

class 내부 getter를 사용했으며 double precision이라는 데이터타입을 사용했다는 것이다.

 

 

import { IsEmail } from "class-validator";
import {
  BaseEntity,
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn
} from "typeorm";

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

  @Column({ type: "text", unique: true })
  @IsEmail()
  email: string;

  @Column({ type: "boolean", default: false })
  verifiedEmail: boolean;

  @Column({ type: "text" })
  firstName: string;

  @Column({ type: "text" })
  lastName: string;

  @Column({ type: "int" })
  age: number;

  @Column({ type: "text" })
  password: string;

  @Column({ type: "text" })
  phoneNumber: string;

  @Column({ type: "boolean", default: false })
  verifiedPhonenNumber: boolean;

  @Column({ type: "text" })
  profilePhoto: string;

  @Column({ type: "boolean", default: false })
  isDriving: boolean;

  @Column({ type: "boolean", default: false })
  isRiding: boolean;

  @Column({ type: "boolean", default: false })
  isTaken: boolean;

  @Column({ type: "double precision", default: 0 })
  lastLng: number;

  @Column({ type: "double precision", default: 0 })
  lastLat: number;

  @Column({ type: "double precision", default: 0 })
  lastOrientation: number;

  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }

  @CreateDateColumn() createdAt: string;
  @UpdateDateColumn() updatedAt: string;
}

export default User;

 

 

Listener

 

기타 Listener Subscriber에 대한 내용은 다음 문서를 참고하자.

github.com/typeorm/typeorm/blob/master/docs/listeners-and-subscribers.md

 

typeorm/typeorm

ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Elect...

github.com

 

특정 이벤트에 반응하기 위한 리스너이다. 종류도 몇개 없고, 데코레이션 명칭도 명확해서 어떤 동작을 캐치할 수 있는지 쉽게 알 수 있다.

 

 

 

위에 작성한 entity에 password 암호화를 위해 listener를 활용해보자.

우선 password 암호화를 위해 bcrypt 패키지를 설치해보자

yarn add bcrypt
yarn add @types/bcrypt --dev

 

플레인한 비밀번호를 받아 hash + salt 화를 진행한다. (bcrypt를 활용한 해쉬, 솔트) 관련 내용은 전에 작성한 포스트에서 확인할 수 있다. 참고로 첨부한 포스트에서는 salt를 생성하는 함수를 별도로 만들었지만 아래는 상수를 넣어서 간단히 처리했다. 

 

암호화를 위해 Listener인 @BeforeInsert()와 @BeforeUpdate()를 사용하였다.

import { IsEmail } from "class-validator";
import bcrypt from "bcrypt";
import {
  BaseEntity,
    BeforeInsert,
    BeforeUpdate,
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn
} from "typeorm";

const BCRYPT_ROUNDS = 10;

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

  ... 중략
    
  private hashPassword(password: string): Promise<string> {
    return bcrypt.hash(password, BCRYPT_ROUNDS)
  }
    
  @BeforeInsert()
  @BeforeUpdate()
  async savePassword(): Promise<void>  {
    if (this.password) {
        const hashedPassword = await this.hashPassword(this.password)
        this.password = hashedPassword
    }
  }
}

export default User;

 

 

* cautions!

 

User entity에는 @BeforeUpdate() 리스너를 달아뒀는데 아래와 같은 update로는 해당 리스너가 트리거되지 않습니다. 

await User.update({ id: user.id }, { ...notNull });

 

왜냐하면 리스너는 인스턴스가 업데이트 될 때 반영되는 것이지, 엔티티 메서드를 사용한다고 해서 반영되는 것이 아니기 때문입니다.  다시 말해 아래와 같이 User 엔티티의 인스턴스를 변화시키는 꼴에서야 반응한다는 것입니다.

 const user = User.findone({id});
 user.password = 1;
 user.save();

 

만약 @BeforeUpdate 부분에 비밀번호 해쉬화와 같은 중요한 로직을 달아뒀다면 문제가 되겠지요. 따라서 별도의 로직을 만들어서 처리해야 합니다. 이 부분은 프로젝트마다 다를 것이므로 각자 특성에 맞게 수정해야 합니다.

 

음... 추후 참고를 위해 일단 현재 작성한 방법을 남겨놓습니다.

const user: User = context.req.user;

const notNull: InotNull = {};

Object.keys(args).forEach((key) => {
  if (args[key] !== null) {
    notNull[key] = args[key];
  }
});

// @BeforeUpdate 리스너 사용을 위해 인스턴스 업데이트를 하기 위함
if (notNull.password !== null && args.password !== null) {
  user.password = args.password;
  user.save(); // 인스턴스를 조작하였으므로 @BeforeUpdate가 트리거 됨
  delete notNull.password; // 평문 password를 저장하면 안되므로 notNull 객체에서 지워주자
}