rimo030 / nestjs-e-commerce-frame

✏️ NestJS로 구현한 Commerce API
44 stars 1 forks source link

Prisma (Migrate from TypeORM) #95

Open rimo030 opened 3 weeks ago

rimo030 commented 3 weeks ago

Prisma

꾸준히 인기를 얻어 작년 하반기 TypeORM의 다운로드 수를 넘긴, Prisma(프리즈마)를 도입해보려 합니다.

image

TypeORM의 문제

타입세이프 하지 않다!

TypeORM은 Entity를 만들어 DB의 각 테이블을 객체로 추상화해 사용합니다.

그러나 아래와 같이 타입추론이 제대로 되지 않는 경우가 발생합니다.

모두 타입세이프 하지 않아서 발생하는 문제입니다.

실제로는 존재하지 않는 데이터인데 컴파일 단계에서 확인할 수 없으니, 타입스크립트를 사용하는 의미가 전혀 없는 상황이 됩니다. 🤔

Prisma는 다릅니다

Prisma는타입세이프한 ORM입니다. 위와 같은 문제점이 발생하지 않습니다.

다른 차이점은 TypeORM보다 추상화가 되어있어, SQL과 거리가 좀 더 먼 문법을 사용한다는 점입니다.

저는 Nest를 배우기 전까지는 쌩쿼리(?)를 사용했었기에 TypeORM도 충분히 개발친화적이라고 느꼈었는데요. Prisma는 그것보다 더 추상화가 잘 되어있다니 기대가 됩니다. ✌️

rimo030 commented 3 weeks ago

Migrate from TypeORM

공식문서를 따라 마이그레이션을 시도합니다.

1. Prisma CLI설치

루트에서 아래 명령어를 입력합니다.

npm install prisma --save-dev

2. Prisma 설정

프리즈마는 schema.prisma라는 파일에 DB 커넥션 정보와 모델을 정의합니다.

아래 명령어로 루트에 prisma 디렉토리를 생성하고 하위에 schema.prisma을 추가할 수 있습니다.

npx prisma init
rimo030 commented 2 weeks ago

3. DB 연결

init 명령어로 생성된 schema.prisma는 기본적으로 아래와 같이 커넥션 정보를 담고 있습니다.

저는 mysql을 사용할 것이라 provider를 변경해 주었습니다.

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

.env파일에는 아래와 같이 커넥션 문자열을 정의합니다.

DATABASE_URL="데이터베이스종류://계정이름:비밀번호@호스트:포트/데이터베이스명"

예를들어 다음과 같은 데이터 베이스 연결정보가 있다면,

{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "myUser",
  "password": "myPassword",
  "database": "commerce"
}

이렇게 작성하면 됩니다.

DATABASE_URL="mysql://myUser:myPassword@localhost:3306/commerce"

env("DATABASE_URL")는 프로젝트에 전역적으로 ConfigModule이 설정되어있다면, 정상적으로 작동할 것입니다.

아래는 제 프로젝트에 설정된 ConfigModule입니다.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    AuthModule,
    ConfigModule.forRoot({
      cache: true,
      isGlobal: true,
    }),
  ],
})
export class AppModule {}
rimo030 commented 2 weeks ago

4. DB pull 받기

현재 우리의 DB에는 TypeORM의 Entity를 바탕으로 생성된 테이블들이 존재합니다.

이를 프리즈마가 읽어 모델로 변환해 줍니다. 이를 pull이라고 합니다. 아래 명령어를 입력합니다.

npx prisma db pull

schema.prisma를 확인해보면 추가된 모델들을 확인할 수 있습니다.

저는 다음과 같은 내용이 추가되어있습니다.

rimo030 commented 2 weeks ago

5. 스키마 조정하기

이제 pull받은 내용을 컨벤션에 맞게 수정합니다.

VSCode를 사용하신다면, 수정전 Prisma 익스텐션을 설치하는 것을 추천드립니다.

image

예시로 프로젝트의 product모델을 가져왔습니다.

조정전의 모델입니다.

model product {
  id                      Int                       @id @default(autoincrement())
  created_at              DateTime                  @default(now()) @db.DateTime(6)
  updated_at              DateTime                  @default(now()) @db.DateTime(6)
  deleted_at              DateTime?                 @db.DateTime(6)
  seller_id               Int
  bundle_id               Int?
  category_id             Int
  company_id              Int
  is_sale                 Int                       @db.TinyInt
  name                    String                    @db.VarChar(128)
  description             String?                   @db.VarChar(255)
  delivery_type           String                    @db.VarChar(128)
  delivery_free_over      Int?
  delivery_charge         Int
  img                     String                    @db.VarChar(255)
  cart                    cart[]
  order_product           order_product[]
  category                category                  @relation(fields: [category_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_0dce9bc93c2d2c399982d04bef1")
  seller                  seller                    @relation(fields: [seller_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_79a3ae0442388a2418ec67a3120")
  company                 company                   @relation(fields: [company_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_a0503db1630a5b8a4d7deabd556")
  product_bundle          product_bundle?           @relation(fields: [bundle_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_f35662270f8bcb3f26dfd6e9fda")
  product_option          product_option[]
  product_required_option product_required_option[]

  @@index([category_id], map: "FK_0dce9bc93c2d2c399982d04bef1")
  @@index([seller_id], map: "FK_79a3ae0442388a2418ec67a3120")
  @@index([company_id], map: "FK_a0503db1630a5b8a4d7deabd556")
  @@index([bundle_id], map: "FK_f35662270f8bcb3f26dfd6e9fda")

조정후의 모델입니다.

model Product {
  id                        Int                       @id @default(autoincrement())
  createdAt                 DateTime                  @default(now()) @db.DateTime(6) @map("created_at")
  updatedAt                 DateTime                  @default(now()) @db.DateTime(6) @map("updated_at")
  deletedAt                 DateTime?                 @db.DateTime(6) @map("deleted_at")
  sellerId                  Int                       @map("seller_id")
  bundleId                  Int?                      @map("bundle_id")
  categoryId                Int                       @map("category_id")
  companyId                 Int                       @map("company_id")
  isSale                    Int                       @db.TinyInt @map("is_sale")
  name                      String                    @db.VarChar(128)
  description               String?                   @db.VarChar(255)
  deliveryType              String                    @db.VarChar(128) @map("delivery_type")
  deliveryFreeOver          Int?                      @map("delivery_free_over")
  deliveryCharge            Int                       @map("delivery_charge")
  img                       String                    @db.VarChar(255)
  carts                     Cart[]
  orderProducts             OrderProduct[]
  category                  Category                  @relation(fields: [categoryId], references: [id], onDelete: NoAction, onUpdate: NoAction)
  seller                    Seller                    @relation(fields: [sellerId], references: [id], onDelete: NoAction, onUpdate: NoAction)
  company                   Company                   @relation(fields: [companyId], references: [id], onDelete: NoAction, onUpdate: NoAction)
  productBundle             ProductBundle?            @relation(fields: [bundleId], references: [id], onDelete: NoAction, onUpdate: NoAction)
  productOptions            ProductOption[]
  productRequiredOptions    ProductRequiredOption[]

  @@index([categoryId])
  @@index([sellerId])
  @@index([companyId])
  @@index([bundleId])
  @@map("product")
}
rimo030 commented 2 weeks ago

6. DB 반영하기

이제 앞으로 모델이나 스키마를 수정한 후에는 push 명령어를 사용할 수 있습니다.

npx prisma db push
rimo030 commented 2 weeks ago

7. NestJS에서 프리즈마 사용하기

아래 패키지를 설치합니다.

npm install @prisma/client

API를 이용하기 위해 prisma.service.ts를 다음과 같이 추가합니다.

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
}

다른 모듈에서도 사용 가능 하도록 모듈로 만들어 보겠습니다.

다음과 같이 서비스를 내보낼수 있도록prisma.module.ts을 작성합니다.

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

그리고 사용하고자 하는 모듈에 import 합니다.

저는 기존의 TypeORM이있던 Auth모듈에 추가를 했습니다. (관련커밋)

rimo030 commented 2 weeks ago

8. 서비스로직에 반영하기

이제 프리즈마 API를 사용해 기존의 서비스로직을 고쳐나가면 됩니다.

@Injectable()
export class AuthService {
  constructor(private readonly prisma: PrismaService,) {}
...
}

잠깐 사용해보았는데, 컬럼 타입 추론 가능해지니 정말 편하네요...✌️

메소드들은 더 사용해봐야 익숙해지겠지만, TypeORM 0.3과 문법자체가 그리 차이나진 않는 것 같습니다.