typeORM, sequelize, primsa 등등 다양한 ORM tool 들이 존재한다.
그 중에서 오늘의 주인공인 Prisma 에 대해서 알아보도록 하자.
ORM
Object Relational Mapping, 객체와 데이터베이스의 관계를 매핑해주는 도구이다.
Prisma는 데이터베이스를 쉽게 사용할 수 있게 도와주는 데이터베이스 toolkit 이다.
ORM과 같은 목적을 가지고 있지만 다른 방식으로 문제를 해결하고있다.
Prisma는 크게 다음 세 가지로 구성되어있다.
여담으로, 우리가 사용하고 있는 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
는 다양한 유형의 코드나 아티팩트를 자동으로 생성하는 역할을 한다. 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
database 와 관련된 속성들을 정의한다
provider
db 종류를 정의한다 ( postgresql, mongodb )
url
db url 을 기입한다
각각의 데이터베이스에 맞는 양식들은 여기서 확인 할 수 있다.
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 와 같은 상황에서는 다음과 같은 옵션들을 사용 할 수 있다
[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
prisma format 명령어를 통해서 schema.prisma 파일을 일정한 포멧으로 유지 할 수 있다.
$ npx prisma format
model test {
col1 String
col2 String
@@unique([ col1, col2 ])
}
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient({
datasources: {
db: {
url: 'file:./dev_qa.db',
},
},
})
const userCount = await prisma.user.count()
select 는 해당 컬럼만 표기하고, include 는 해당 컬럼을 추가해서 반환한다.
https://www.prisma.io/docs/reference/api-reference/prisma-client-reference
$ npx prisma migrate dev --name "init"
## production
$ npx prisma migrate deploy
https://www.prisma.io/blog/nestjs-prisma-rest-api-7D056s1BmOL0#migrate-the-database
await prisma.$disconnect();
prisma
를 사용하다보면 위와 같이 $
로 시작하는 함수들을 마주하게 된다.
당황스러운가? 나는 그랬다
보통 $
로 시작하는 메서드는 개발자에게 명시적으로 prisma
내부의 특정 동작을 수행한다는 것을 나타내기 위해 사용된다. 따라서 $disconnect()
메서드는 데이터베이스 연결 해제 작업을 수행하는 prisma
의 내부 메서드로서, $
접두사가 해당 메서드가 prisma
내부 동작을 나타내는 것임을 알려준다
하단의 경우 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 의 경우, 그룹화없이 단일 집계 결과를 반환한다.
profile 이 user 에 의존하고 있는 경우 아래와 같이 한번에 생성 할 수 있다.
await prisma.user.create({
data: {
name: 'John Doe',
profile: {
create: {
bio: 'Hello, I am John Doe'
}
}
}
});
query 를 할때 값이 지정되지 않은 값을 가져오려면 어떻게해야할까?
this.test.find({
where: {
name: {
isSet: false
}
},
둘 다 사용중인 이름을 다른 이름과 매핑 시키는 역할은 동일하다. 다만 사용되는 대상이 다른데,
@map
은 field 단에서 사용하며, @@map
은 table 단에서 사용한다.
https://www.prisma.io/docs/reference/api-reference/error-reference
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
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 를 생성 할 수 있다.