본문 바로가기

javascript/nodejs

[nodejs] prisma ORM 라이브러리

https://www.prisma.io/

 

Prisma | Next-generation ORM for Node.js & TypeScript

Prisma is a next-generation Node.js and TypeScript ORM for PostgreSQL, MySQL, SQL Server, SQLite, MongoDB, and CockroachDB. It provides type-safety, automated migrations, and an intuitive data model.

www.prisma.io

  prisma는 타입스크립트 환경에서 사용 가능한 ORM 라이브러리 중 하나로, 모델의 정의를 자바스크립트 또는 타입스크립트 파일에 선언하는 대신 prisma schema라는 별도의 설정 파일을 이용하여 처리한다는 점이 인상적이다.


타 ORM과의 비교

 sequelize나 typeorm 등 다른 ORM들은 일반적으로 자바스크립트 또는 타입스크립트 파일 내에서 사용자가 데이터베이스 상의 엔티티나 관계를 코드로 구현하며, 정의된 모델에 대한 프로토타입 또는 클래스 인스턴스를 생성하는 방식으로 동작하고는 한다.


Sequelize 예시

import {
    DataTypes,
    BelongsToGetAssociationMixin,
    BelongsToSetAssociationMixin,
    BelongsToCreateAssociationMixin,
    BelongsToManyGetAssociationsMixin,
    BelongsToManyCountAssociationsMixin,
    BelongsToManyHasAssociationMixin,
    BelongsToManyHasAssociationsMixin,
    BelongsToManySetAssociationsMixin,
    BelongsToManyAddAssociationMixin,
    BelongsToManyAddAssociationsMixin,
    BelongsToManyRemoveAssociationMixin,
    BelongsToManyRemoveAssociationsMixin,
    BelongsToManyCreateAssociationMixin
} from 'Sequelize';
import { ICart, ICartInstance } from '../Interface/ICart.interface.js';
import { ICartItemInput } from '../Interface/ICartItem.interface.js';
import { IProductPrimaryKey } from '../Interface/IProduct.interface.js';
import { IUserPrimaryKey } from '../Interface/IUser.interface.js';
import { sequelize as db } from '../util/database.js';
import { CartItem } from './cart-item.model.js';
import { Product } from './product.model.js';
import { User } from './user.model.js';

export class Cart extends ICartInstance implements ICart {
    declare id: string;

    /*with User*/
    declare getUser: BelongsToGetAssociationMixin<User>;
    declare setUser: BelongsToSetAssociationMixin<User, IUserPrimaryKey>;
    declare createUser: BelongsToCreateAssociationMixin<User>;

    /*with Product*/
    declare getProducts: BelongsToManyGetAssociationsMixin<Product>;
    declare countProducts: BelongsToManyCountAssociationsMixin
    declare hasProduct: BelongsToManyHasAssociationMixin<Product, IProductPrimaryKey>
    declare hasProducts: BelongsToManyHasAssociationsMixin<Product, IProductPrimaryKey>
    declare setProducts: BelongsToManySetAssociationsMixin<Product, IProductPrimaryKey>
    declare addProduct: BelongsToManyAddAssociationMixin<Product, IProductPrimaryKey>
    declare addProducts: BelongsToManyAddAssociationsMixin<Product, IProductPrimaryKey>
    declare removeProduct: BelongsToManyRemoveAssociationMixin<Product, IProductPrimaryKey>
    declare removeProducts: BelongsToManyRemoveAssociationsMixin<Product, IProductPrimaryKey>
    declare createProduct: BelongsToManyCreateAssociationMixin<Product>;
    declare cartitem: CartItem | ICartItemInput; // 단순 객체로 사용해야 되는 경우가 존재함...
}

Cart.init({
    id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true
    }
}, {
    sequelize: db,
    modelName: 'cart'
});

 Sequelize은 Model 클래스를 상속받아 init 함수에 실제 테이블 정의를 작성한다.

두 엔티티 사이의 연관 관계(1:1, 1:N, N:N)가 존재할 때 두 관계 사이에 대응되는 함수를 런타임에 구성해 주는데, 자바스크립트 환경에서는 상당히 편리하다. 다만 해당 함수들이 컴파일 타임에 인식되지 않아 자동완성 기능이 제공되지 않으며, 타입스크립트 환경에서는 별도의 정의가 없는 경우 오류로 간주하기 때문에 사용성은 상당히 나쁘다.

 타입스크립트 환경에서 Sequelize을 굳이 사용하고 싶은 경우, 위 코드처럼 사용자가 declare을 통해 두 엔티티 사이를 연결하는 함수들을 선언하여 타입스크립트 환경에서 인식할 수 있도록 설정하는 과정이 필요하다. 이때 엔티티 및 연관 관계의 수가 늘어날수록 타입을 인식하기 위한 설정 작업이 크게 증가하기 때문에, 관련된 코드를 자동으로 생성해 주는 별도의 툴이 추가되지 않는 이상 쉽사리 손이 가지 않을 라이브러리다.


typeorm 예시

import { Column, Entity,
	JoinColumn, ManyToOne, 
	OneToMany, PrimaryGeneratedColumn, Relation } from 'typeorm';
import { IsInt } from 'class-validator';
import { DetailedLecture } from './detailed_lec.entity.js';
import { University } from './university.entity.js';
import { ITetroInfo } from '../../interface/tetro_pool.interface.js';

/**
 * @description 파이썬 기반 스크래핑을 통해 얻은 코드를 작성된 테트로미노 더미.
 */
@Entity()
export class TetroPool implements ITetroInfo {
    /**
     * 테트로미노 풀의 id
     */
    @PrimaryGeneratedColumn()
    @IsInt()
    id!: number;

    /**
     * 테트로미노 풀의 이름
     */
    @Column()
    name: string;

    /**
     * 테트로미노 풀에 대한 설명
     */
    @Column()
    description : string;

    @ManyToOne(() => University, univ => univ.tetro_pools, {onDelete:'CASCADE', onUpdate:'CASCADE'})
    univ! : Relation<University>;


    @OneToMany(() => DetailedLecture, (dl) => dl.tetroPool)
    lectures? : Relation<DetailedLecture>[];

    constructor(name: string, description: string)
    {
        this.name = name;
        this.description = description;
    }
}

  typeorm은 데코레이터 문법을 적극적으로 활용하여 클래스 내에 속성 및 연관 관계를 설정하는 ORM으로, nestjs 프레임워크에서 공식적으로 지원하고 있다. 


prisma의 경우

 위 언급한 ORM들은 사용자가 타입스크립트 환경이 제공하는 문법을 이용하여 클래스 기반 모델을 구성하게 된다. 반면   prisma는 각 모델을 클래스로 구현하는 게 아니라, prisma schema 설정 파일 내에 별도의 문법을 이용하여 정의한다. 이 문법의 경우 E-R 다이어그램에 사용되는 문법과 유사한 점이 많으므로 직관적으로 모델 및 연관 관계를 구성할 수 있다.

 사용되는 언어 환경과 분리된 독립적인 문법을 이용하여 모델 및 관계 자체에만 집중할 수 있으면서도, 표현 자체가 간결하여 모델 사이의 관계를 묘사하기 쉽다는 점을 장점으로 들 수 있겠다.

// schema.prisma 파일 내부
generator client {
  provider = "prisma-client-js"
}

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

model Product {
  id          Int     @id @default(autoincrement())
  title       String  @db.VarChar(255)
  price       Float
  description String? @db.Text
  imageUrl    String? @db.VarChar(255)
  uid         Int?

  orderitems OrderItem[]
  cartitems  CartItem[] // many-to-many explicitly
  user       User?       @relation(fields: [uid], references: [id])

  @@map("products")
}

model User {
  id    Int    @id @default(autoincrement())
  name  String
  email String

  products Product[]
  cart     Cart?
  order    Order[]
}
model Cart {
  id  Int @id @default(autoincrement())
  uid Int @unique

  items CartItem[] // many-to-many explicitly
  user  User       @relation(fields: [uid], references: [id])
}

model CartItem {
  cid      Int
  pid      Int
  quantity Int
  cart     Cart    @relation(fields: [cid], references: [id], onDelete: Cascade, onUpdate: Cascade)
  product  Product @relation(fields: [pid], references: [id], onDelete: Cascade, onUpdate: Cascade)

  @@id([cid, pid])
}

model Order {
  id  Int @id @default(autoincrement())
  uid Int

  items OrderItem[] // many-to-many explicitly
  user  User        @relation(fields: [uid], references: [id])

  @@map("orders")
}

model OrderItem {
  oid      Int
  pid      Int
  quantity Int
  order    Order   @relation(fields: [oid], references: [id])
  product  Product @relation(fields: [pid], references: [id])

  @@id([oid, pid])
}

  그러나 prisma schema는 타입스크립트 환경과 관계없는 별도의 설정 파일에 불과하므로, 기본적으로 개발 환경에 영향을  주지 않기 때문에 해당 파일만으로는 설계한 모델을 프로젝트 내에서 사용할 수 없다. 이에 대해 prisma는 사용자가 설계한 모델을 프로젝트에 반영하기 위한 방법을 제공한다.


prisma의 동작 방식

 prisma는 위에서 언급한 것처럼 모델의 정의를 클래스처럼 타입스크립트 환경 대신 prisma schema (schema.prisma)라는 별도의 설정 파일 내에 정의하기 때문에, 기본적으로 자바스크립트에서 동작하지 않는다. 대신 해당 설정 파일을 기반으로 쿼리를 수행하는 쿼리 빌더, 데이터베이스와 상호작용하는 CLI가 존재한다.

간략한 prisma 라이브러리의 구조 ( 일부 차이가 존재할 수 있음 )

 prisma 라이브러리를 구성하는 요소는 다음과 같다.

  • prisma schema: 모델 및 데이터베이스 정보가 담긴 설정 파일
  • prisma: prisma schema에 대한 여러 가지 동작을 수행하는 데 사용되는 CLI
  • @prisma/client: prisma schema를 이용하여 데이터베이스를 연결하는 쿼리 빌더

CLI 명령어

prisma generate

https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/generating-prisma-client

 

Generating the client (Concepts)

This page explains how to generate Prisma Client. It also provides additional context on the generated client, typical workflows and Node.js configuration.

www.prisma.io

prisma schema 내의 모델에 대한 타입 선언을 node_modules/@prisma/client 내에 생성하는 과정

npx prisma generate // npm 환경
yarn prisma generate // yarn 환경

 prisma generate 명령은 사용자가 prisma schema 파일 내에 만들어 둔 모델에 대한 클라이언트를 만들 때 사용한다.

 prisma schema는 앞서 언급했듯이 단순한 설정 파일이기 때문에 실제 개발 환경과 무관하므로, 이를 실제 데이터베이스에 대해 이용하기 위해서는 대응되는 클라이언트 시스템을 생성해야 한다. prisma generate 명령은 스키마를 기반으로 스키마 내 각 모델에 대한 인터페이스에 대한 선언(declaration) 파일(*.d.ts)을 생성하고, 쿼리 엔진을 클라이언트 모듈 내에 복사하는 등 작업을 진행한다.

 @prisma/client을 처음 설치한 상태에서는 클라이언트 측에서 모델을 인식하지 못한다. prisma schema 상에 정의한 모델이 개발되는 환경에 반영되지 않았기 때문이다. 그 이유는 @prisma/client 모듈 자체는 일종의 껍데기이기 때문이다.

@prisma/client 내부에 선언된 파일들. 모두 .prisma/client라는 다른 모듈의 요소를 가져오고 있다.

 위 코드에 보이는 것처럼 @prisma/client 자체는 단순히 node_modules/.prisma/client라는 또 다른 모듈을 참조하기 위한  래퍼 모듈이다. node_modules 폴더 내를 뒤져보면 . prisma 모듈이 따로 존재하는 것을 볼 수 있다.

.prisma/client 폴더 내의 파일들
generator의 output 값을 변경하면 저장되는 경로를 바꿀 수 있다.

 해당 모듈 내의 파일들은 3가지 부류로 나뉜다.

  1. 클라이언트, 모델 타입 등 자동 생성된 파일
  2. 쿼리 엔진
  3. prisma schema을 복사한 파일

클라이언트, 모델 타입 등 자동 생성된 파일

자동으로 생성된 모델 관련 파일들

 첫 번째 부류의 파일들은 외부로 노출하기 위한 클라이언트 관련 정보를 지정하거나, 사용자가 작성한 모델 및 연관 관계를 기반으로 클라이언트에서 사용할 타입 목록을 생성한다. 위 prisma schema 코드를 사용한 프로젝트에서 생성한 타입 관련 코드는 대략 1만 라인에 달한다. 이러한 타입들은 @prisma/client을 통해 import 가능하므로 따로 모델의 프로퍼티에 대한 인터페이스 및 타입을 구현할 필요가 없다.

 생성된 코드 대부분이 클라이언트 관련 정보보다는 타입 구현 관련 정보에 치중해 있는 것을 고려하면, generate 명령의 목적이 주로 사용자의 편의성을 위한 모델 타입 지원에 가까워 보인다.

쿼리 엔진

https://www.prisma.io/docs/concepts/components/prisma-engines/query-engine 

 

Query engine (Concepts)

Prisma's query engine manages the communication with the database when using Prisma Client. Learn how it works on this page.

www.prisma.io

 위 글에서 볼 수 있듯이 prisma client에는 쿼리 엔진이라는 요소가 포함된다. 쿼리 엔진은 @prisma/engines 폴더에 바이너리 파일의 형태로 존재하며, rust 프로그래밍 언어 기반으로 작성되었다고 한다.

쿼리 엔진의 위치

 prisma generate 명령을 수행하면 해당 바이너리 파일은 .prisma/client 폴더 아래로 복사된다.

스키마 파일

 스키마 파일은 prisma generate 명령을 수행한 시점의 prisma schema를 복사한 것이다. 해당 파일의 용도를 정확히 설명하기는 힘들지만, 아마 다음 prisma generate을 수행했을 때 두 시점의 스키마 사이에 변경사항이 존재하는지 검사하기 위한 목적으로 사용되지 않을까 짐작한다.


prisma db push/pull

prisma db pull 명령을 이용하여 이미 존재하는 데이터베이스 정보를 가져오는 경우

// 데이터베이스에 prisma schema을 반영할 때
npx prisma db push
yarn prisma db push

// 데이터베이스의 릴레이션을 기반으로 prisma schema을 생성할 때
npx prisma db pull
yarn prisma db pull

 prisma db push/pull 명령은 데이터베이스와 prisma schema 사이의 상호작용과 관련되며, 이름 같은 기능을 가진다.

  • prisma db push: prisma schema을 기반으로 지정된 데이터베이스 내에 테이블(릴레이션)을 생성한다.
  • prisma db pull: 데이터베이스의 테이블(릴레이션)을 기반으로 prisma schema을 생성한다.

 개인적으로 정말 편리하다고 느꼈던 기능은 prisma db pull 이다. 대다수의 orm은 기존 데이터베이스 내 테이블이 존재하는지 여부와 관계없이 사용자가 직접 각각의 모델에 대응되는 클래스를 구현하고, 이를 클라이언트 인스턴스에게 전달해야만 클라이언트가 해당 테이블을 인식할 수 있는 경우가 많다. 따라서 기존 시스템을 orm 기반으로 전환하는 과정에서 릴레이션의 속성이 누락되거나 관계가 제대로 표현되지 않는 등 문제 발생 가능성이 존재한다.

 반면 prisma의 db pull 명령을 이용하는 경우 라이브러리가 릴레이션의 속성 및 관계 등의 정보를 분석하여 자동으로 모델을 생성해 주기 때문에 사용자에 의한 실수를 배제하고, 모델을 직접 생성하기 위한 수고를 덜을 수 있다. 다른 ORM들이 데이터베이스를 연결할 때 모델 정보를 사용하는 것과는 달리, prisma 라이브러리의 경우 모델과 개발환경을 분리한 덕분에 이러한 동작이 가능했을 것으로 보인다.

 이외에도 prisma CLI와 관련된 많은 옵션이 존재하므로, 필요하다면 공식 문서를 살펴보자.


모델 및 환경 설정

prisma 라이브러리의 workflow. 출처: https://www.prisma.io/docs/concepts/overview/what-is-prisma

  1. 라이브러리 설치
  2. prisma init 명령을 실행하여 prisma schema 관련 파일 생성
  3. prisma schema에 모델 설계
  4. prisma generate 명령을 통해 @prisma/client에 설계한 모델 반영
  5. prisma db push 명령을 통해 데이터베이스 내에 모델 반영
npm install prisma @prisma/client
yarn add prisma @prisma/client

모델 작성 방법

 prisma의 모델 작성 문법은 해당 모델을 SQL로 작성하는 것과 유사한 경험을 준다. 다음 예시를 보자.

model Product {
  id          Int     @id @default(autoincrement())
  title       String  @db.VarChar(255)
  price       Float
  description String? @db.Text
  imageUrl    String? @db.VarChar(255)
  uid         Int?

  orderitems OrderItem[]
  cartitems  CartItem[] // many-to-many explicitly
  user       User?       @relation(fields: [uid], references: [id])

  @@map("products")
}

위 모델 정의는 다음 SQL 문법과 유사하다.

CREATE TABLE products (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,
    price FLOAT NOT NULL,
    description TEXT,
    imageUrl VARCHAR(255) NOT NULL,
    uid INT,
    CONSTRAINT product_uid_fkey
    FOREIGN KEY (uid) REFERENCES User(id)
);

두 코드를 살펴보면 알 수 있듯이 모델을 정의하는 문법 자체가 SQL 구문과 상당히 유사한 구조를 가지고 있으며, 차이점이 있다면 @relation 속성이 없는 orderitems 및 cartitems에 대한 언급이 존재하는 정도이다. 이러한 유사성 덕분에 SQL에 어느 정도 익숙한 경우 자동완성만으로도 기능을 유추하여 모델을 구성할 수 있다.

모델 간 관계

  E-R 표기법에서 사용하는 3가지 관계를 지원한다. 관계는 대응되는 모델의 속성@relation 속성을 통해 표현하며, @relation 속성이 존재하는 쪽에서 외래키를 지정해야 한다. 실제 SQL을 기준으로 생각해 보자면 @relation은 FOREIGN KEY 제약조건에 대응된다고 생각하면 되며, 기존 데이터베이스 설계 규칙에 따라 모델 간 관계를 설계하면 편하다.

  1. 1대 1 관계
  2. 1대 N 관계
  3. M대 N 관계

1대 1 관계

model Student {
  id  Int @id @default(autoincrement())
  firstName String @db.VarChar(32)
  lastName String @db.VarChar(32)
  contactinfo ContactInfo?
}

model ContactInfo {
  uid Int @id
  phoneNo String @db.VarChar(30)
  
  student Student @relation(fields: [uid], references: [id])
}

 위 코드는 학생 및 학생에 대한 연락 정보의 관계를 나타낸다. 학생은 하나의 연락 정보를 가지며, 연락 정보는 하나의 학생에게만 대응된다. 학생은 반드시 연락 정보를 가질 필요가 없으나 연락 정보는 대응되는 학생이 반드시 필요한 Weak Entity로 설정했다.

 이 상황의 경우 필요없는 NULL을 줄이기 위해 항상 관계에 참여하는 ContactInfo 모델 측이  외래키 uid을 가진다. @relation 필드는 외래키 uid를 가진 ContactInfo 모델의 student 속성에 대해 지정된다.

 SQL 기준으로는 다음 코드에 대응된다고 볼 수 있다.

CREATE TABLE Student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    firstName VARCHAR(32) NOT NULL,
    lastName VARCHAR(32) NOT NULL
);

CREATE TABLE ContactInfo (
    uid INT PRIMARY KEY,
    phoneNo VARCHAR(30),
    /* @relation이 FOREIGN KEY 제약조건에 대응된다. */
    CONSTRAINT contactinfo_uid_fkey
    FOREIGN KEY (uid) REFERENCES Student(id) 
);

1대 N 관계

model User {
  id    Int    @id @default(autoincrement())
  name  String @db.VarChar(32)
  email String @db.VarChar(32)

  products Product[]
  cart     Cart?
  order    Order[]
}

model Order {
  id  Int @id @default(autoincrement())
  uid Int

  items OrderItem[] // many-to-many explicitly
  user  User        @relation(fields: [uid], references: [id])

  @@map("orders")
}

 위 코드는 유저와 주문내역의 관계를 나타낸다. 한 유저는 여러 주문을 가질 수 있으며, 주문은 단 한 명의 유저에게만 속하므로 1대 N 관계를 만족한다. 위 상황에서는 다수 측에 해당하는 모델인 Order이 외래키 uid을 가진다. 

 대응되는 코드는 다음과 같다.

CREATE TABLE User (
    id int PRIMARY KEY AUTO_INCREMENT,
    name varchar(32) NOT NULL,
    email varchar(32) NOT NULL
);

CREATE TABLE `orders` (
    id int PRIMARY KEY AUTO_INCREMENT,
    uid int NOT NULL,
    KEY `orders_uid_fkey` (uid),
    CONSTRAINT `orders_uid_fkey` FOREIGN KEY (`uid`) 
    REFERENCES User(id)
    ON DELETE RESTRICT ON UPDATE CASCADE
)

 변환된 코드에서 특징적인 부분은 Order 모델의 이름이다. SQL 상에서 ORDER 키워드는 SELECT을 통해 검색한 결과를 정렬하기 위한 ORDER BY 표현을 위해 미리 예약되어 있으므로 테이블의 이름으로 사용하기에 적합하지 않다. 이러한 이유로 Order 모델 내에는 생성되는 테이블의 이름을 변경하기 위해 @@map( ) 어노테이션이 존재한다.  해당 어노테이션을 사용하면 모델과 대응되는 데이터베이스 상 릴레이션 명을 변경 가능하다.


M대 N 관계

 M대 N 관계는 일반적으로 관계에 해당하는 릴레이션을 두 모델 사이에 추가하여 총 3개 릴레이션이 관계를 가지는 방식으로 구현한다. 예를 들어 인터넷 장바구니의 경우를 생각해보자. 하나의 장바구니에는 여러가지 제품이 포함될 수 있고, 하나의 제품은 여러 장바구니에 담길 수 있으므로 M대 N 관계를 만족한다.

M : N 관계의 표현

prisma에서는 M : N 관계를 2가지 방법으로 구현할 수 있다.

  1. 관계에 대응되는 모델을 생략(prisma가 알아서 처리)
  2. 관계에 대응되는 모델을 명시적으로 표현

 관계에 대한 모델 표현을 생략하는 방식의 경우 @relation이 드러나지 않는다. SQL 에서는 관계에 대한 릴레이션이 두 모델의 기본키를 외래키로 참조하는 구조를 가지는데, 해당 릴레이션을 prisma에서 알아서 처리하므로 두 모델은 상대방 모델에 대한 속성만 가진다.

model Cart {
  id  Int @id @default(autoincrement())
  uid Int @unique

  products Product[]
}

model Product {
  id          Int     @id @default(autoincrement())
  title       String  @db.VarChar(255)
  price       Float
  description String? @db.Text
  imageUrl    String? @db.VarChar(255)
  uid         Int?

  carts Cart[]
}

  관계에 대한 모델을 표현하는 경우는 해당 모델 및 관계를 SQL로 옮겼을 경우와 거의 동일한 구조를 가진다. 이때 관계에 대한 모델은 두 관계에 대한 @relation을 가진다. 아래 CartItem을 참고하자.

model Cart {
  id  Int @id @default(autoincrement())
  uid Int @unique

  items CartItem[] // many-to-many explicitly
}

model CartItem {
  cid      Int
  pid      Int
  quantity Int
  cart     Cart    @relation(fields: [cid], references: [id], onDelete: Cascade, onUpdate: Cascade)
  product  Product @relation(fields: [pid], references: [id], onDelete: Cascade, onUpdate: Cascade)

  @@id([cid, pid])
}

model Product {
  id          Int     @id @default(autoincrement())
  title       String  @db.VarChar(255)
  price       Float
  description String? @db.Text
  imageUrl    String? @db.VarChar(255)
  uid         Int?

  cartitems  CartItem[] // many-to-many explicitly
}

클라이언트 사용

import {PrismaClient} from '@prisma/client';

export const db = new PrismaClient();

/**
 * prisma 클라이언트를 데이터베이스와 연결
 */
export async function dbConn() {
    await db.$connect();
}

// schema.prisma 파일 내부
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

 @prisma/client에서 PrismaClient을 가져온다. 일부 속성을 지정할 수 있기는 한데, 기본적으로 prisma schema의 datasource에 지정된 값들을 따르므로 해당 값을 변경하는게 더 적합하다.  env(~)은 메인 폴더에 생성되어 있는 .env 파일로부터 데이터를 읽어들이므로, 해당 파일 내에 중요한 내용을 저장하는 편이 좋다.

각 모델에 대한 프로퍼티가 생성된 모습

 prisma generate 명령으로 클라이언트를 생성한 경우 모델에 대응되는 프로퍼티를 사용할 수 있다. 각 모델은 기본적으로 CRUD 기능을 지원한다. 자세한 설명은 링크를 참고하자. 애초에 메서드들이 직관적이라, 대충 만져보면 알 수 있다.


트랜잭션

 트랜잭션은 DBMS 내에서 쪼갤 수 없는 작업의 최소 단위로, ACID라는 특징을 가진다. ACID는 다음과 같다.

  • Atomicity(원자성): 트랜잭션을 구성하는 연산들은 모두 수행되거나, 모두 수행되지 않아야 한다.
  • Consistency(일관성): 트랜잭션이 성공한 후에도 실행 이전과 일관성을 유지해야 한다.
    • A -> B로 500원을 이체하는 연산이 진행되었다면, A와 B의 전체 금액 합은 연산 이전과 동일해야 한다.
  • Isolation(독립성): 현재 실행 중인 트랜잭션이 완료될 때까지 어떤 트랜잭션의 연산도 끼어들 수 없다.
    • 트랜잭션 A 실행 도중에 다른 트랜잭션이 A의 연산 중간값을 참조하지 못한다.
  • Durability(지속성): 트랜잭션이 완료되었다면 해당 결과는 데이터베이스에 영구적으로 반영되야 한다.
    • 시스템 장애가 발생하더라도 완료된 트랜잭션 결과는 그대로 남는다

 prisma는 트랜잭션 API을 2가지 방법으로 사용할 수 있다.

  1. 연산 목록을 배열로 전달.
  2. 연산을 수행하는 async 함수 전달.
await db.$transaction([
    db.order.create({
        data: {
            uid: 1
        }
    }),
    db.cart.delete({
        where: {
            uid: 1
        }
    })
]);


async function createOrder(id: number) {
    await db.$transaction(async () => {
        const order = await db.order.create({
            data: {
                uid: 1
            }
        });

        await db.cart.delete({
            where: {
                uid: order!.id!
            }
        })
    })
}

 두가지 방식 중 어떤 것을 사용해도 상관은 없으나, 전자의 경우 prisma에서 기본 제공하는 함수만 포함될 수 있다. 만약 prisma client가 제공하는 함수를 사용하는 다른 함수가 존재하고, 이러한 함수들에 대해 트랜잭션을 적용하고 싶은 경우, 배열 방식으로는 실행할 수 없으므로  후자를 선택한다.

export const postOrder: RequestHandler = async (req, res, next) => {
    const user = req.user;
    if (user) {
        const cart = await CartEntity.getCartEntityByUid(user.id);
        if (cart) {
            db.$transaction(async (tx) => {
                const items = await cart.getCartItems();
                const order = new OrderEntity({ uid: user.id });
                await order.save(items); // 주문 생성
                await cart.deleteProducts();
            });
        }
    }

    res.render('shop/orders', {
        pageTitle: 'Your Orders',
        path: '/orders'
    });
}

 위 코드에서는 interactive transaction(async 함수 기반)을 사용하고 있다. 위 코드처럼 클라이언트를 Data Mapper 패턴 그대로 사용하는 대신 따로 모델 관련 코드를 구현하는 경우, prisma client가 제공하는 기능은 해당 모델의 함수 내에서 실행되기 때문에 배열의 인자로 전달할 수 없으므로 async 함수 내부에서 실행하도록 구현해야 한다.

공식문서에서 트랜잭션 API가 아닌 다른 방식으로 트랜잭션을 다루는 기법들도 소개하고 있으므로, 살펴보길 바란다.

https://www.prisma.io/docs/guides/performance-and-optimization/prisma-client-transactions-guide

 

Transactions

Explore techniques for handling transactions with Prisma Client.

www.prisma.io