database
A
信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
ORMの罠:Prisma・Drizzle・SQLAlchemyの落とし穴
ORM は便利だが、知らないと性能問題・予期せぬクエリ・N+1・デッドロックを引き起こす。Prisma・Drizzle(TypeScript)・SQLAlchemy(Python)の実際の落とし穴と対策を解説します。
一言結論
ORMの性能問題の9割はN+1クエリ・全カラム取得・発行SQLを把握していないことに起因し、includeで明示的なJOINを指定しORMのクエリログを常に確認する習慣だけで大半を防ぐことができる。
ORM の共通の落とし穴
落とし穴1: N+1 問題
// ❌ Prisma で N+1 を発生させる
const users = await prisma.user.findMany();
for (const user of users) {
const posts = await prisma.post.findMany({ where: { userId: user.id } });
}
// → 1(users取得) + N(userごとのposts取得)本のクエリ
// ✅ include でJOINに変換する
const users = await prisma.user.findMany({
include: { posts: true },
});
// → 1〜2本のクエリで完了
落とし穴2: 必要以上のカラムを取得
// ❌ 全カラム取得(100MB のblobカラムも含まれる場合も)
const users = await prisma.user.findMany();
// ✅ 必要なカラムだけを select
const users = await prisma.user.findMany({
select: { id: true, name: true, email: true },
});
落とし穴3: ORM が発行する SQL を把握していない
// Prisma でログを有効にする
const prisma = new PrismaClient({
log: ["query", "info", "warn", "error"],
});
// または特定のクエリだけ確認
const users = await prisma.user.findMany().$queryRawUnsafe("..."); // ← 使い方間違い
// EXPLAIN ANALYZE で確認
Prisma の固有の落とし穴
大量データの取得
// ❌ 大量データを一度に取得するとメモリ枯渇
const allLogs = await prisma.log.findMany(); // 100万件を一気に取得
// ✅ カーソルベースのページング
const PAGE_SIZE = 1000;
let cursor: number | undefined;
while (true) {
const logs = await prisma.log.findMany({
take: PAGE_SIZE,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { id: "asc" },
});
if (logs.length === 0) break;
await processLogs(logs);
cursor = logs[logs.length - 1].id;
}
トランザクションとデッドロック
// ❌ ネストしたトランザクションで意図せずデッドロック
await prisma.$transaction(async (trx) => {
await trx.order.update({ where: { id: 1 }, data: { status: "processing" } });
await trx.inventory.update({ where: { id: 1 }, data: { count: { decrement: 1 } } });
});
// ✅ 更新順序を統一する(デッドロックを防ぐ)
// 常に「order → inventory」の順で更新するルールを決める
Prisma の connect と create の使い分け
// ❌ 間違い: 存在しない ID で connect するとエラー
await prisma.post.create({
data: {
title: "記事",
author: { connect: { id: 999 } }, // 存在しない ID
},
});
// ✅ 事前に存在確認するか、upsert を使う
Drizzle ORM の落とし穴
型安全でも SQL を理解する必要がある
import { eq, and, inArray } from "drizzle-orm";
// ❌ 間違いやすいクエリ(WHERE なしで全件更新)
await db.update(users).set({ role: "admin" }); // ← WHERE を書き忘れた!
// ✅ WHERE を明示する
await db.update(users).set({ role: "admin" }).where(eq(users.id, userId));
// IN クエリ
await db.select().from(users).where(inArray(users.id, [1, 2, 3]));
Drizzle の JOIN
// ✅ Drizzle での JOIN
const result = await db
.select({
postId: posts.id,
postTitle: posts.title,
authorName: users.name,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.isPublished, true))
.orderBy(desc(posts.createdAt))
.limit(10);
SQLAlchemy の落とし穴
Lazy Loading が N+1 を引き起こす
# ❌ lazy loading (デフォルト) で N+1
users = session.query(User).all()
for user in users:
print(user.posts) # ここで user ごとにクエリが発行される
# ✅ joinedload でまとめて取得
from sqlalchemy.orm import joinedload
users = session.query(User).options(joinedload(User.posts)).all()
Session の使い方
# ❌ セッションを閉じ忘れる
session = Session()
user = session.query(User).first()
# session を閉じ忘れると接続プールが枯渇
# ✅ context manager を使う
with Session() as session:
user = session.query(User).first()
# with ブロックを出ると自動でクローズ
まとめ
| 問題 | ORM | 対策 |
|---|---|---|
| N+1 | 全て | include / joinedload / eager loading |
| 過剰取得 | 全て | select で必要カラムだけ指定 |
| SQL 把握 | 全て | ログを有効化・EXPLAIN で確認 |
| 大量データ | Prisma | カーソルベースのページング |
| Lazy Loading | SQLAlchemy | joinedload・selectinload を使う |
ORM は SQL を隠蔽しますが、発行される SQL を理解した上で使うことが性能問題を防ぐ最善策です。
参考: Prisma 公式 / Drizzle 公式 / SQLAlchemy 公式