OSSのTypeScript ORM比較:Drizzle vs Prisma vs TypeORM で型安全なDB操作を実現する
オープンソースラボ編集部 ・ 2026年6月14日
OSSのTypeScript ORM比較:Drizzle vs Prisma vs TypeORM で型安全なDB操作を実現する
Laravel Eloquent(PHP)・ActiveRecord(Ruby)のJavaScript世界における対応ツールとして、Drizzle ORM(最軽量・SQLに近いTypeScript ORM)・Prisma(最もポピュラーなNode.js ORM)・TypeORM(デコレータベース・JavaのJPAに近い)はOSSのTypeScript/JavaScript ORMです。
ORMが解決する問題
SQLを直接書くとエラーが実行時まで見つからない・型が不安定になります:
- 型安全なクエリ: テーブルカラム名のtypo→コンパイルエラーで事前検出
- マイグレーション管理: スキーマ変更をバージョン管理してチームで共有
- リレーション管理: JOINを型安全なInclude/Joinで表現
- DB非依存: コードを変えずにPostgreSQL/MySQLを切り替え
主要ツールの概要
Drizzle ORM
2023年にリリースされたTypeScript製の軽量ORMです。GitHubスター25k+。SQLに最も近いTypeScript ORMで最速です。スキーマ定義がTypeScriptコード(pgTable()等)で行われ、生成されたSQLが予測可能です。バンドルサイズが小さくEdge Runtime(Vercel Edge・Cloudflare Workers)で動作し、Drizzle Kitで自動マイグレーション生成が可能です。
// schema.ts - Drizzle スキーマ定義
import { pgTable, text, timestamp, integer, boolean, uuid, varchar } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey(),
email: varchar("email", { length: 255 }).notNull().unique(),
name: text("name"),
role: text("role", { enum: ["user", "admin", "editor"] }).default("user").notNull(),
emailVerified: boolean("email_verified").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const posts = pgTable("posts", {
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
slug: text("slug").notNull().unique(),
content: text("content"),
status: text("status", { enum: ["draft", "published", "archived"] }).default("draft"),
authorId: uuid("author_id").notNull().references(() => users.id, { onDelete: "cascade" }),
viewCount: integer("view_count").default(0).notNull(),
publishedAt: timestamp("published_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const categories = pgTable("categories", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
slug: text("slug").notNull().unique(),
});
export const postCategories = pgTable("post_categories", {
postId: uuid("post_id").notNull().references(() => posts.id),
categoryId: uuid("category_id").notNull().references(() => categories.id),
});
// リレーション定義
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
postCategories: many(postCategories),
}));
// db.ts - データベース接続
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });
// lib/posts.ts - クエリ関数
import { db } from "@/lib/db";
import { posts, users, categories, postCategories } from "@/lib/schema";
import { eq, desc, and, count, sql } from "drizzle-orm";
// 公開記事一覧(JOIN・フィルタ・ページネーション)
export async function getPublishedPosts(page = 1, limit = 10) {
const offset = (page - 1) * limit;
return db
.select({
id: posts.id,
title: posts.title,
slug: posts.slug,
publishedAt: posts.publishedAt,
authorName: users.name,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.status, "published"))
.orderBy(desc(posts.publishedAt))
.limit(limit)
.offset(offset);
}
// withでリレーションを自動JOINする(Prismaのincludeに相当)
export async function getPostWithRelations(slug: string) {
return db.query.posts.findFirst({
where: eq(posts.slug, slug),
with: {
author: { columns: { id: true, name: true, email: true } },
postCategories: {
with: { category: true },
},
},
});
}
// 集計クエリ
export async function getPostStats() {
const [result] = await db
.select({
total: count(),
published: count(sql`CASE WHEN ${posts.status} = 'published' THEN 1 END`),
totalViews: sql<number>`SUM(${posts.viewCount})`,
})
.from(posts);
return result;
}
// インサート(戻り値も型安全)
export async function createPost(data: typeof posts.$inferInsert) {
const [post] = await db.insert(posts).values(data).returning();
return post;
}
// アップデート
export async function publishPost(id: string) {
const [updated] = await db
.update(posts)
.set({ status: "published", publishedAt: new Date() })
.where(eq(posts.id, id))
.returning();
return updated;
}
# drizzle-kit でマイグレーション管理
# package.json の scripts に追加
# "db:generate": "drizzle-kit generate",
# "db:migrate": "drizzle-kit migrate",
# "db:push": "drizzle-kit push", # 開発時(マイグレーションファイル不要)
# "db:studio": "drizzle-kit studio"
# drizzle.config.ts
# import { defineConfig } from "drizzle-kit";
# export default defineConfig({
# schema: "./src/lib/schema.ts",
# out: "./migrations",
# dialect: "postgresql",
# dbCredentials: { url: process.env.DATABASE_URL! },
# });
# スキーマ変更からマイグレーションファイルを生成
npx drizzle-kit generate
# マイグレーションを実行
npx drizzle-kit migrate
# Drizzle Studio (ブラウザUIでDBを確認)
npx drizzle-kit studio
# WebUI: https://local.drizzle.studio
Prisma
2019年に公開されたTypeScript製のOSSデータベースツールキットです。GitHubスター40k+。Prisma Schema(独自DSL)でスキーマを定義し、Prisma Clientを自動生成します。型安全性が非常に高く、VSCodeでのオートコンプリートが快適。Prisma Migrateでマイグレーション管理、Prisma Studioでデータブラウジングができます。ただしバンドルサイズが大きくEdge Runtimeは限定的です。
// schema.prisma - Prismaスキーマ定義
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
name String?
role Role @default(USER)
emailVerified Boolean @default(false)
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum Role { USER ADMIN EDITOR }
model Post {
id String @id @default(uuid())
title String
slug String @unique
content String?
status PostStatus @default(DRAFT)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
categories Category[] @relation("PostCategories")
viewCount Int @default(0)
publishedAt DateTime?
createdAt DateTime @default(now())
}
enum PostStatus { DRAFT PUBLISHED ARCHIVED }
model Category {
id String @id @default(uuid())
name String
slug String @unique
posts Post[] @relation("PostCategories")
}
// lib/prisma.ts - Prisma Clientのシングルトン(Next.jsのHMR対応)
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query", "warn", "error"] : ["error"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
// 記事一覧(Prismaスタイル)
const posts = await prisma.post.findMany({
where: { status: "PUBLISHED" },
include: {
author: { select: { name: true, email: true } },
categories: true,
},
orderBy: { publishedAt: "desc" },
take: 10,
skip: 0,
});
機能比較表
| 比較項目 | Drizzle ORM | Prisma | TypeORM |
|---|---|---|---|
| スキーマ定義 | TypeScript | 独自DSL | デコレータ |
| Edge Runtime | ✅(設計から) | 限定的 | ❌ |
| バンドルサイズ | 34KB | 数MB | 数MB |
| マイグレーション | drizzle-kit | prisma-migrate | 組み込み |
| Studio UI | ✅ | ✅ | ❌ |
| GitHub Stars | 25k+ | 40k+ | 34k+ |
ORMで取得したデータをAPIとして公開するにはSecurityカテゴリ/categories/securityのAuth.jsで認証を追加します。DBマイグレーションのCI/CDにはDevOpsカテゴリ/categories/devopsのGitHub ActionsでPull Request単位でマイグレーションをチェックします。
FAQ
Q. DizzleとPrismaどちらを新規Next.jsプロジェクトで採用すべきですか?
A. 現在(2026年)の推奨はDrizzle ORMです。理由: ①Edge Runtime(Vercel Edge Functions・Cloudflare Workers)で動く②バンドルサイズが小さい(Vercel/CloudflareのコールドスタートにPrismaは重い)③SQL力が身につく設計(ORMが生成するSQLが予測しやすい)④PrismaのスキーマDSLより純粋なTypeScriptで書ける。Prismaを選ぶ理由: ①チームがDrizzleを知らないがPrismaは知っている②Prismaのより豊富なドキュメント・コミュニティが必要③Mongooseに慣れたチームがPostgresに移行する場合。
Q. DrizzleでN+1クエリ問題を防ぐには?
A. db.query.posts.findMany({ with: { author: true } })のようなwith句(リレーション)を使えばDrizzleが自動的に2つのSELECT(posts→users)に分解してN+1を回避します。もしくはleftJoinを使って1クエリで完結させます。パフォーマンス確認はEXPLAIN ANALYZEをDrizzle Studioのクエリビルダーで実行するのが最も簡単です。
Q. PrismaからDrizzleへの移行は難しいですか?
A. スキーマ変換は比較的簡単: schema.prismaからschema.ts(Drizzle)への変換ツールがコミュニティに存在します。既存のマイグレーションは既にDB反映済みなので、Drizzleのdrizzle-kit introspectで既存DBからスキーマを逆生成できます。クエリはAPIが異なるため書き直しが必要ですが、Copilot/Claudeを使えば機械的な変換が効率化されます。新規ファイルから始めて既存Prismaコードを並行で残しながら徐々に移行するのが安全です。
Q. Next.js App RouterのServer ActionでDrizzleを使う例を教えてください
A. Server Actionはサーバーサイドのみで動くためDrizzleを直接呼べます。
// app/blog/new/actions.ts
"use server";
import { db } from "@/lib/db";
import { posts } from "@/lib/schema";
import { auth } from "@/auth";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPostAction(formData: FormData) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
const title = formData.get("title") as string;
const content = formData.get("content") as string;
const slug = title.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]/g, "");
const [post] = await db.insert(posts).values({
title, content, slug,
authorId: session.user.id!,
status: "draft",
}).returning({ id: posts.id, slug: posts.slug });
revalidatePath("/blog");
redirect(`/blog/${post.slug}`);
}
まとめ
| ユースケース | 推奨ツール |
|---|---|
| Next.js・Edge Runtime・SQLファースト | Drizzle ORM |
| 豊富なドキュメント・Prisma生態系 | Prisma |
| Javaライクなデコレータ・엔터프라이즈バックエンド | TypeORM |