はじめに
直近のプロジェクトにおけるTypeScriptでのバックエンド実装の際に、ORMライブラリであるPrismaとDrizzleをそれぞれ使用する機会がありました。
今回は両者の使用感について比較も交えつつまとめてみたいと思います。
Prisma, Drizzleの特徴
最初にPrismaとDrizzleそれぞれの特徴についてざっくり紹介します。
- Prisma – https://www.prisma.io/
- Drizzle – https://orm.drizzle.team
| Prisma | Drizzle | |
|---|---|---|
| スキーマファイル | schema.prismaという専用ファイル | schema.tsというTypeScriptファイル |
| TypeScript型情報 | schema.prismaを元にprisma generateコマンドで型情報およびクライアントを生成 | schema.tsファイルがそのまま型情報になる |
| スキーマ → DB | prisma migrateコマンドでマイグレーションファイル生成&適用 | drizzle-kit generateコマンドでマイグレーションファイル生成drizzle-kit migrateコマンドで適用 |
| DB → スキーマ | prisma db pullコマンドで既存DBからschema.prismaファイルを生成 | drizzle-kit pullコマンドで既存DBからschema.tsファイルを生成 |
| クエリの記述 | 生成されたクライアントを使用してORMらしく簡潔に書ける | ORMっぽさは薄くSQLに近い書き味 |
| 複雑なクエリ | Prisma APIで表現できないクエリはRaw SQLやTypedSQLで生のSQLを記述 | sqlテンプレートを使って生のSQL断片を柔軟に差し込める |
実際の使用感
ここからは簡単なサンプルも交えて、実際の使用感について書いていきたいと思います。
直近で携わったプロジェクトでは既存のDBを正とし、DBからスキーマファイルを自動生成する方針だったので、マイグレーション関連の使用感は割愛します。
スキーマ定義
スキーマ定義について、簡単なサンプルとして「記事」「ユーザー」「コメント」のスキーマ定義を考えてみます。
簡略化のためスキーマファイルの定義部分のみ抜粋しています。
Prisma
schema.prisma
model User {
id Int @id @default(autoincrement())
name String
posts Post[]
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
authorId Int @map("author_id")
title String
body String
titleEmbedding Unsupported("vector")? @map("title_embedding")
published Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
author User @relation(fields: [authorId], references: [id])
comments Comment[]
@@map("posts")
}
model Comment {
id Int @id @default(autoincrement())
postId Int @map("post_id")
body String
createdAt DateTime @default(now()) @map("created_at")
post Post @relation(fields: [postId], references: [id])
@@map("comments")
}
schema.prismaという専用ファイルでのスキーマ定義- このファイルを元に
prisma generateコマンドで、型情報の出力とクライアント生成を行う - 独自形式ではあるが内容は直感的でわかりやすい
Drizzle
schema.ts
export const users = pgTable("users", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
});
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
authorId: integer("author_id")
.references(() => users.id)
.notNull(),
title: text("title").notNull(),
body: text("body").notNull(),
titleEmbedding: vector("title_embedding", { dimensions: 4 }),
published: boolean("published").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const comments = pgTable("comments", {
id: serial("id").primaryKey(),
postId: integer("post_id")
.references(() => posts.id)
.notNull(),
body: text("body").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
後述するRelational Query API(rqb v2)を使用する場合は、スキーマ定義に加えて以下のようにdefineRelationsを使った関連定義を追加します。
import { defineRelations } from "drizzle-orm";
import * as schema from "./schema";
export const relations = defineRelations(schema, (r) => ({
users: {
posts: r.many.posts({
from: r.users.id,
to: r.posts.authorId,
}),
},
posts: {
author: r.one.users({
from: r.posts.authorId,
to: r.users.id,
}),
comments: r.many.comments({
from: r.posts.id,
to: r.comments.postId,
}),
},
comments: {
post: r.one.posts({
from: r.comments.postId,
to: r.posts.id,
}),
},
}));
schema.tsというTypeScriptファイルでのスキーマ定義Zodに似たメソッドチェーンでのカラム定義- TypeScriptのコードとしてスキーマを定義するため、型情報がそのままスキーマ定義と直結している
- Relational Query APIを使用する場合は
defineRelationsによる関連定義が別途必要
クエリの記述
簡単なコードサンプルも交えつつ、クエリの書き味についても紹介していきます。
1) 公開記事一覧を取得(著者名つき・新着10件)
Prisma
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
body: true,
createdAt: true,
author: {
select: {
id: true,
name: true,
},
},
},
where: { published: true },
orderBy: { createdAt: "desc" },
take: 10,
});
- ORMらしい書き味で関連取得が直感的
- 型安全で補完も効く
Drizzle
const postRows = await db
.select({
id: posts.id,
title: posts.title,
body: posts.body,
createdAt: posts.createdAt,
authorId: users.id,
authorName: users.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(10);
Drizzle(Relational Query API)
※ 以下の記述は Relational Query API(rqb v2)の記法を前提にしています。
const postRows = await db.query.posts.findMany({
columns: {
id: true,
title: true,
body: true,
createdAt: true,
},
with: {
author: {
columns: {
id: true,
name: true,
},
},
},
where: {
published: true,
},
orderBy: {
createdAt: "desc",
},
limit: 10,
});
- 基本はSQLに近い書き味だが、読み取り系のクエリに関してはRelational Query APIを使用することでORMらしい簡潔な記述も可能
- Relational Query APIを使用する場合は、事前に
relationsの定義が必要 - 型安全で補完も効く
2) 記事作成 + 初回コメント作成をトランザクションで一括実行
Prisma
const created = await prisma.$transaction(async (tx) => {
const post = await tx.post.create({
data: {
authorId: 1,
title: "Prisma vs Drizzle",
body: "PrismaとDrizzleの比較メモです。",
published: false,
},
select: { id: true, title: true, body: true },
});
await tx.comment.create({
data: {
postId: post.id,
body: "first comment",
},
});
return post;
});
- 一貫してORMらしい書き味で直感的
Drizzle
const created = await db.transaction(async (tx) => {
const [post] = await tx
.insert(posts)
.values({
authorId: 1,
title: "Prisma vs Drizzle",
body: "PrismaとDrizzleの比較メモです。",
published: false,
})
.returning({ id: posts.id, title: posts.title, body: posts.body });
await tx.insert(comments).values({
postId: post.id,
body: "first comment",
});
return post;
});
- SQLライクな書き味
3) タイトルと入力パラメータのコサイン類似度が高い順に5件取得
今回のプロジェクトではベクトル検索を行う必要があったのですが、そこでの使用感に大きな違いがあったので、こちらもコードサンプルを交えて紹介したいと思います。
※ コサイン類似度 = 1 - コサイン距離()
Prisma(Raw SQL)
const queryEmbedding = [0.12, -0.03, 0.44, 0.08];
const queryEmbeddingStr = `[${queryEmbedding.join(",")}]`; // vector型にバインドするため文字列に変換
const posts = await prisma.$queryRaw`
SELECT
p.id,
p.title,
p.body,
1 - (p.title_embedding ${queryEmbeddingStr}::vector) AS similarity
FROM posts p
WHERE p.published = true
ORDER BY similarity DESC
LIMIT 5
`;
Prisma(TypedSQL)
prisma/sql/searchSimilarPosts.sql
SELECT
p.id,
p.title,
p.body,
1 - (p.title_embedding $1::vector) AS similarity
FROM posts p
WHERE p.published = true
ORDER BY similarity DESC
LIMIT 5;
TypeScript側の利用例
import { searchSimilarPosts } from "@prisma/client/sql";
const queryEmbedding = [0.12, -0.03, 0.44, 0.08];
const posts = await prisma.$queryRawTyped(
searchSimilarPosts(queryEmbedding), // ← SQLファイルに書いたクエリを呼び出せる関数が生成される
);
- ベクトル検索のようなクエリはPrisma APIで表現できないため、Raw SQLかTypedSQLで生のSQLを記述することになる
- Raw SQLは文字列として生のSQLを書くことになるので、型安全性や補完は効かない
- TypedSQLはSQLファイルに生のSQLを書き、
prisma generate --sqlコマンドを実行することでTypeScriptから呼び出せる型安全な関数が生成される
Drizzle
const queryEmbedding = [0.12, -0.03, 0.44, 0.08];
const distance = cosineDistance(posts.titleEmbedding, queryEmbedding); // Drizzleにはコサイン距離を計算するヘルパーが用意されている
const similarity = sql`1 - ${distance}`;
// ヘルパーを使わない場合でも、生のSQL断片を型付きで部分的に差し込める
// const similarity = sql`1 - (${posts.titleEmbedding} ${queryEmbedding}::vector)`;
const rows = await db
.select({
id: posts.id,
title: posts.title,
body: posts.body,
similarity: similarity,
})
.from(posts)
.where(eq(posts.published, true))
.orderBy(desc(similarity))
.limit(5);
- 元からSQLに近く、書き味は大きく変わらない
sqlテンプレートを使用して柔軟に生のSQL断片を差し込める
まとめ
PrismaのORMらしい書き味は、シンプルなCRUDや関連取得などでは非常に直感的で書きやすいと感じました。
一方で、今回のベクトル検索のようにPrisma APIで表現できないクエリや、実際に発行されるSQLを厳密にコントロールしたい場合などは、結局生のSQLを書く必要が出てきます。
そのような場合に書き味が大きく変わってしまう上、型安全性を保持しようとするとひと手間かかるのが少し気になりました。
Drizzleは基本的にSQLに近い書き味ですが、読み取り系のクエリに関してはRelational Query APIを使用することで、ORMらしい簡潔な記述を行うことも可能です。
SQLの知識がある前提であれば基本となるSQLライクな記述に統一すれば学習コストも低く、どのような場合も一貫した書き味で型安全に書ける点が良かったです。
ここまでの個人的な使用感を踏まえると、使い分けるのであれば以下のようになるかなと思います。
| 重視するポイント | おすすめ |
|---|---|
| SQL知識がそこまでなくても書きやすい方が良い | Prisma |
| 単純なCRUDや関連取得が中心 | Prisma |
| SQLに慣れている | Drizzle |
| 複雑なクエリやSQLの細かいチューニングが必要で、SQLライクな書き味を一貫させたい | Drizzle |
以上、ORMライブラリの選定の際に少しでも参考になれば幸いです。








