prisma
는 스스로 Next-generation
Typescript orm 이라고 할만큼 강력한 타입 안정성과 비교적 간단한 스키마 작성 등 좋은 개발자 경험을 제공하는 ORM 입니다. 하지만 다른 ORM 대비 여러 단점도 가지고 있습니다.
Lock
을 지원하지 않는다.다중 생성
시 생성된 레코드를 반환 받을 수 없다.RETURNING
구문을 사용할 수 있는 옵션이 없다.kysely
extension 사용하기해당 포스트의 모든 예시는
Bun
런타임에서 작성되었으나node
환경에서도 마찬가지로 사용할 수 있습니다. 전체 코드 예시는 여기 에서 확인하실 수 있습니다.
강력한 타입 안정성을 제공하는 typescript sql 빌더 입니다. (orm이 아닙니다) 참고
bun install kysely prisma-extension-kysely prisma-kysely pg
schema.prisma
kysely generator 설정generator kysely {
provider = "prisma-kysely"
}
bun db:generate
)하면 자동으로 스키마 정의에 따라 prisma의 하위 경로(generated/types.ts
)에 아래와 같은 kysely
를 위한 typescript 타입 정의 파일이 생성된다/** prisma/generated/types.ts */
import type { ColumnType } from "kysely";
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
export type Comment = {
id: Generated<number>;
text: string;
post_id: number;
user_id: number;
created_at: Timestamp;
updated_at: Timestamp;
};
export type Post = {
id: Generated<number>;
title: string;
created_at: Timestamp;
updated_at: Timestamp;
author_id: number;
likes: Generated<number>;
};
export type PostLikes = {
id: Generated<number>;
post_id: number;
user_id: number;
created_at: Timestamp;
updated_at: Timestamp;
};
export type User = {
id: Generated<number>;
name: string;
};
export type DB = {
comments: Comment;
post_likes: PostLikes;
posts: Post;
users: User;
};
prisma-extension-kysely
의 kyselyExtension
를 통해 prismClient
를 확장한다. 이 때, prisma-kysely
를 통해 자동 생성된 타입을 kysely의 generic 타입으로 설정하여 사용할 수 있다. import { PrismaClient } from "@prisma/client";
import {
Kysely,
PostgresAdapter,
PostgresIntrospector,
PostgresQueryCompiler,
} from "kysely";
import kyselyExtension from "prisma-extension-kysely";
import type { DB } from "./generated/types";
export const prisma = new PrismaClient({
log: ["query"],
}).$extends(
kyselyExtension({
kysely: (driver) =>
new Kysely<DB>({
dialect: {
createDriver: () => driver,
createAdapter: () => new PostgresAdapter(),
createIntrospector: (db) => new PostgresIntrospector(db),
createQueryCompiler: () => new PostgresQueryCompiler(),
},
}),
})
);
PrismaClient
의 인스턴스에 $kysely
속성이 추가된다.Prisma
의 raw query 기능을 활용하면 아래처럼 코드를 작성하게 되는데 코드를 작성할 때 부터 hint
지원을 받을 수 없을 뿐만 아니라 SQL injection
공격을 막기 위한 parameter 변환 구문도 불편하고, 응답 타입 또한 제네릭을 설정하지 않으면 unknown
으로 설정되기 때문에 타입으로 인한 런타임 버그의 확률이 늘어나게 된다.
public async findMany(ids: number[]) {
return await prisma.$queryRaw(
Prisma.sql`
SELECT
id as "id",
title as "title",
created_at as "createdAt",
updated_at as "updatedAt",
author_id as "authorId"
FROM posts as p
INNER JOIN users as u ON p.author_id = u.id
WHERE id IN (${Prisma.join(ids)})
ORDER BY p.id DESC
`
);
}
TypeORM
등의 다른 ORM에서 제공하는 쿼리 빌더를 사용해봤다면 익숙한 방식으로 빌더 패턴을 사용해서 쿼리 생성할 수 있다.join
으로 쿼리를 생성할 수 있고, 다양한 옵션으로 prisma
에서는 힘든 복잡한 쿼리를 작성할 수 있다. public async findMany(count = 20): Promise<Post[]> {
return await prisma.$kysely
.selectFrom("posts as p")
.innerJoin("users as u", "p.author_id", "u.id")
.select([
"p.id as id",
"p.title as title",
"p.created_at as createdAt",
"p.updated_at as updatedAt",
"u.id as authorId",
"u.name as authorName",
])
.orderBy("p.id", "desc")
.limit(count)
.execute()
.then((rows) =>
rows.map((row) =>
Post.of({
id: row.id,
title: row.title,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
author: {
id: row.authorId,
name: row.authorName,
},
})
)
);
}
const res = await prisma.$kysely.selectFrom("posts as p").leftJoin(
(qb) =>
qb.selectFrom("users as u")
.select(["u.name", "u.id"])
.as("author"),
(join) => join.onRef("p.author_id", "=", "author.id")
);
무엇보다 DX가 좋은 점은 아래에서 확인할 수 있는 것처럼 테이블이름이나 필드명을 aliasing을 하더라도 ide에서 aliasing 한 문자에 따라 hint
를 제공받을 수 있고, 타입으로 인한 런타임 버그를 줄일 수 있다.
또한 select
에서 aliasing
한 필드명으로 결과 type이 지정되어 타입이 명확하지 않아 발생할 수 있는 런타임 버그를 줄일 수 있다.
kysely
도 자체적으로 transaction 기능을 지원하지만 위의 공식 문서 의 transaction 항목에서 명시되어 있는 것처럼 kysely
의 transaction
메서드를 직접 사용하는 것은 지원하지 않고 prisma
의 $transaction
스코프에서 콜백 인자의 $kysely
를 사용해야 한다. $kysely
속성이 존재한다.$kysely
와 기존 prisma
의 메서드를 같이 사용해도 트랜잭션의 원자성이 보장된다. public async findIdWithLock(
id: number,
tx: PrismaTransactionManager
): Promise<number | null> {
return await tx.$kysely
.selectFrom("posts as p")
.select("p.id")
.where("p.id", "=", id)
.forUpdate() // 이외에도 forShare, forKeyShare 등 메서드 지원
.execute()
.then((rows) => {
return rows.length > 0 ? rows[0].id : null;
});
}
public async createMany(
posts: (Omit<IPost, "id" | "author"> & { authorId: number })[]
): Promise<Post[]> {
const ids = await prisma.$kysely
.insertInto("posts")
.values(
posts.map((p) => ({
author_id: p.authorId,
title: p.title,
created_at: p.createdAt,
updated_at: p.updatedAt,
}))
)
.returning("id") // 이외에 returningAll() 메서드 지원
.execute()
.then((rows) => rows.map((row) => row.id))
prisma
는 매우 강력한 타입을 지원하는 ORM
이지만 서비스를 개발하면서 복잡한 쿼리나 쿼리 최적화를 해야만 하는 일이 꽤나 자주 발생하기 때문에 단점도 꽤나 분명하다고 느꼈는데, kysely
확장을 통해 해당 단점들을 많이 상쇄할 수 있다고 생각한다. prisma
자체적으로 database level join
기능을 preview 기능을 최근에 추가해줘서 간단한 join을 위해서는 쿼리 결과를 transform 하는 개발 비용을 줄이기 위해서prisma
의 것을 사용하는게 더 좋지 않을까 생각한다.lateral join
을 사용하고 있었다. 해당 부분은 다른 포스팅에서 다뤄볼 예정