Prisma

Younghwan Cha·2023년 8월 22일
0
post-thumbnail

typeORM, sequelize, primsa 등등 다양한 ORM tool 들이 존재한다.
그 중에서 오늘의 주인공인 Prisma 에 대해서 알아보도록 하자.

ORM
Object Relational Mapping, 객체와 데이터베이스의 관계를 매핑해주는 도구이다.

Prisma 가 뭐야?


Prisma는 데이터베이스를 쉽게 사용할 수 있게 도와주는 데이터베이스 toolkit 이다.
ORM과 같은 목적을 가지고 있지만 다른 방식으로 문제를 해결하고있다.
Prisma는 크게 다음 세 가지로 구성되어있다.

  • Prisma Client: Prisma server 와 상호작용을 하는 Prisma Client
  • Prisma Migrate: Django의 migration과 같이 데이터베이스 모델 선언 가능
  • Prisma Studio: 데이터베이스의 데이터를 수정할 수 있도록 Prisma에서 제공해주는 GUI

여담으로, 우리가 사용하고 있는 prisma 는 prisma2 인데, prisma1 과 prisma2 의 차이에 대해서
궁금하다면 아래 블로그를 참고하도록 하자
https://gmyankee.tistory.com/265?category=1084683

[prisma docs] https://www.prisma.io/docs/concepts/overview/what-is-prisma

시작하기


$ npm i -D prisma
$ npx prisma init

위 명령어를 통해서 .env 파일과 아래 prisma/schema.prisma 이 생성된다.

// prisma/schema.prisma

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

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

generator prismaClassGenerator {
  provider = "prisma-class-generator"
  output = "../.gen/response"
  dryRun = false
  separateRelationFields = true
}

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

model User {
  id   String   @id @default(uuid()) @db.ObjectId
  name String
}

prisma schema 는 위에서 보이는 것과 같이 generator, datasource, model 로 구성된다

generator

generator 는 다양한 유형의 코드나 아티팩트를 자동으로 생성하는 역할을 한다. output에 지정된 경로는 Prisma CLI가 prisma generate 명령을 실행할 때 Prisma 클라이언트 라이브러리가 생성되는 위치를 나타낸다.

prisma-client-js

Prisma 에서 기본적으로 제공하는 generator 로, Prisma Client를 위한 JavaScript/TypeScript 클라이언트 라이브러리를 생성한다. 이를 통해 데이터베이스와의 상호 작용을 타입 안전하게 수행할 수 있다.

prisma-class-validator-generator

Prisma 모델을 기반으로 Class Validator 데코레이터와 함께 TypeScript 클래스를 생성한다.

[prisma validator generator github] https://github.com/omar-dulaimi/prisma-class-validator-generator

datasource

database 와 관련된 속성들을 정의한다

  • provider
    db 종류를 정의한다 ( postgresql, mongodb )

  • url
    db url 을 기입한다

각각의 데이터베이스에 맞는 양식들은 여기서 확인 할 수 있다.

model

model 에서는 사용할 table 들을 정의한다

model Test {
  @@map("test")

  id                 String               @id @default(uuid())
  academyTag         AcademyTag[]
}

model TestTag {
  @@map("test_tag")

  id                 String     @id @default(uuid())
  name               String     @unique
  tagId              String     @db.ObjectId
  tagModel           Tag        @relation(fields: [tagId], references: [id], onDelete: SetNull, onUpdate: Cascade)
}

onUpdate / onDelete 와 같은 상황에서는 다음과 같은 옵션들을 사용 할 수 있다

  • Cascade
  • Restrict
  • NoAction
  • SetNull
  • SetDefault

[prisma referential actions docs] https://www.prisma.io/docs/concepts/components/prisma-schema/relations/referential-actions

const academy = await this.prismaService.academy.create({
      data: {
        ...data
        Tag: {
          create: list.map((id) => ({ tagId: id })),
        },
      },
});

적용하기

하단 명령어를 통해서 @unique 와 같은 제약사항들을 적용시킬 수 있다

npx prisma db push

formatting

prisma format 명령어를 통해서 schema.prisma 파일을 일정한 포멧으로 유지 할 수 있다.

$ npx prisma format

Prisma Schema

model test {
	col1  String
    col2  String
    
	@@unique([ col1, col2 ])
}

Prisma Client


import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient({
  datasources: {
    db: {
      url: 'file:./dev_qa.db',
    },
  },
})

Count

const userCount = await prisma.user.count()

select vs include

select 는 해당 컬럼만 표기하고, include 는 해당 컬럼을 추가해서 반환한다.

https://www.prisma.io/docs/reference/api-reference/prisma-client-reference

Prisma Migration


$ npx prisma migrate dev --name "init"

## production
$ npx prisma migrate deploy

https://www.prisma.io/blog/nestjs-prisma-rest-api-7D056s1BmOL0#migrate-the-database

prisma 의 $

await prisma.$disconnect();

prisma 를 사용하다보면 위와 같이 $ 로 시작하는 함수들을 마주하게 된다.
당황스러운가? 나는 그랬다
보통 $ 로 시작하는 메서드는 개발자에게 명시적으로 prisma 내부의 특정 동작을 수행한다는 것을 나타내기 위해 사용된다. 따라서 $disconnect() 메서드는 데이터베이스 연결 해제 작업을 수행하는 prisma 의 내부 메서드로서, $ 접두사가 해당 메서드가 prisma 내부 동작을 나타내는 것임을 알려준다

methods

  • findFirst
    지정한 옵션에 맞는 첫번째 record 를 반환한다
  • findFirstOrThrow

groupBy

하단의 경우 user 들을 country 별로 그룹화 한 후에 profileViews 값을 더해서 반환한다

const groupUsers = await prisma.user.groupBy({
  by: ['country'],
  _sum: {
    profileViews: true,
  },
})

# result

[
  { country: 'Germany', _sum: { profileViews: 126 } },
  { country: 'Sweden', _sum: { profileViews: 0 } },
]

1:N 의 경우에는 아래와 같이 _count 를 할 수 있다.

const usersWithCount = await prisma.user.findMany({
  include: {
    _count: {
      select: { posts: true },
    },
  },
})

참고로, groupBy 사용시 relation field 사용이 불가능하다
https://github.com/prisma/prisma/issues/16243

추가적으로, schema 에 int type 이 존재할 경우에만 groupBy 에서 _sum 을 사용 할 수 있다.

groupBy vs aggregate?
집계 함수를 사용하는 방식에는 groupBy 와 aggregate 두가지 방식이있다.
groupBy 의 경우, 특정 필드 기준으로 그룹화하고, 그룹별로 집계를 수행할 수 있게 해준다.
반면에 aggregate 의 경우, 그룹화없이 단일 집계 결과를 반환한다.

create

profile 이 user 에 의존하고 있는 경우 아래와 같이 한번에 생성 할 수 있다.

await prisma.user.create({
  data: {
    name: 'John Doe',
    profile: {
      create: {
        bio: 'Hello, I am John Doe'
      }
    }
  }
});

null / undefined 에 대한 처리 - isSet

query 를 할때 값이 지정되지 않은 값을 가져오려면 어떻게해야할까?

this.test.find({
  where: {
    name: {
      isSet: false
    }
  },

@map & @@map

둘 다 사용중인 이름을 다른 이름과 매핑 시키는 역할은 동일하다. 다만 사용되는 대상이 다른데,
@map 은 field 단에서 사용하며, @@map 은 table 단에서 사용한다.

Error 처리

https://www.prisma.io/docs/reference/api-reference/error-reference

Transaction


transaction 을 사용하는 두가지 방법이 있다

const [posts, totalPosts] = await prisma.$transaction([
  prisma.post.findMany({ where: { title: { contains: 'prisma' } } }),
  prisma.post.count(),
])
async function transfer(from: string, to: string, amount: number) {
  return await prisma.$transaction(async (tx) => {
    // 1. Decrement amount from the sender.
    const sender = await tx.account.update({
    })

    // 2. Verify that the sender's balance didn't go below zero.
    if (sender.balance < 0) {
      throw new Error(`${from} doesn't have enough to send ${amount}`)
    }

    // 3. Increment the recipient's balance by amount
    const recipient = await tx.account.update({
    })

    return recipient
  })
}

한가지 알아둘 점이 있다면, prisma transaction 의 경우 PrismaPromise<any>[] 를 인자로 받기 때문에 async/await 이나 Promise.all 을 사용할 경우 에러가 발생하니 유의하도록 하자

[prisma transaction blog] https://velog.io/@kimjiwonpg98/prisma-%EC%82%AC%EC%9A%A9%EA%B8%B0-3-transaction

Type


UncheckedInput

model Post {
  id       Int     @id @default(autoincrement())
  title    String  @db.VarChar(255)
  content  String?
  author   User    @relation(fields: [authorId], references: [id])
  authorId Int
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

위의 경우를 보자

// PostUncheckedCreateInput
prisma.post.create({
  data: {
    title: 'First post',
    content: 'Welcome to the first post in my blog...',
    authorId: 1,
  },
})

// PostCreateInput
prisma.post.create({
  data: {
    title: 'First post',
    content: 'Welcome to the first post in my blog...',
    author: {
      connect: {
        id: 1,
      },
    },
  },
})

UnChecked 의 경우, 조금 더 자유도높게 쿼리를 작성 할 수 있다. 하지만 큰 자유도에는 큰 위험이 따르는 것 처럼 authorId=1 이 존재하여야 한다.
아래의 경우에는 connect 대신 conenctOrCreate 를 통해서 없는 record 를 생성 할 수 있다.

profile
개발 기록

0개의 댓글