はじめに

直近のプロジェクトにおけるTypeScriptでのバックエンド実装の際に、ORMライブラリであるPrismaとDrizzleをそれぞれ使用する機会がありました。
今回は両者の使用感について比較も交えつつまとめてみたいと思います。

Prisma, Drizzleの特徴

最初にPrismaとDrizzleそれぞれの特徴についてざっくり紹介します。

PrismaDrizzle
スキーマファイルschema.prismaという専用ファイルschema.tsというTypeScriptファイル
TypeScript型情報schema.prismaを元にprisma generateコマンドで型情報およびクライアントを生成schema.tsファイルがそのまま型情報になる
スキーマ → DBprisma 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ライブラリの選定の際に少しでも参考になれば幸いです。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる