困ったこと
一覧画面で投稿とコメントを表示する際、Prismaのincludeでリレーション先に条件を付けて取得しようとしたが、期待通りにフィルタされた子レコードが取得できず、空配列が返される問題が発生した。
具体的には以下のような一覧取得を実現したかった:
- 公開済みの投稿一覧
- 各投稿に対して削除されていないコメントのみ
- コメントは作成日時の降順でソート
// Prismaで理想的だった処理(うまくいかなかった)
const posts = await prisma.post.findMany({
where: { published: true },
include: {
comments: {
where: { isDeleted: false },
orderBy: { createdAt: 'desc' },
},
},
})
しかし実際には、includeの条件がうまく効かずコメントが空配列で返されることが多発。
一時的な回避策
仕方なく以下の手順で実装していた:
- 投稿データを先に取得
- 取得した投稿のID一覧でコメントを再取得
- サーバー側でフィルタリングとソート処理
- アプリ側で手動マージ
// 実際に使っていた二段クエリでの回避策
const posts = await prisma.post.findMany({
where: { published: true }
})
const postIds = posts.map(p => p.id)
// 全コメントを取得
const allComments = await prisma.comment.findMany({
where: { postId: { in: postIds } }
})
// サーバー側でフィルタリング・ソート処理
const filteredComments = allComments
.filter(comment => !comment.isDeleted)
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
// アプリケーション側で手動マージ
const postsWithComments = posts.map(post => ({
...post,
comments: filteredComments.filter(comment => comment.postId === post.id)
}))
問題点
- DBとサーバー間の通信が多く発生
- 不要なデータも含めて全件取得してからフィルタリング
- サーバー側でのメモリ消費とCPU負荷が増大
- 件数が増えるほど処理が重くなる
- ビジネスロジックがデータアクセス層に混入
SQL直書きで解決
prisma.$queryRawUnsafeを使って、直接SQLクエリを書くことで解決した。
const rows = await prisma.$queryRawUnsafe<any[]>(`
SELECT
p.*,
c.comment_id,
c.body,
c.created_at as comment_created_at
FROM t_post p
LEFT JOIN t_comment c
ON c.post_id = p.post_id AND c.is_deleted = false
WHERE p.is_deleted = false
ORDER BY p.created_at DESC, c.created_at DESC
LIMIT 100
`)
// アプリ側で親子をグルーピング
const postsMap = new Map()
rows.forEach(row => {
if (!postsMap.has(row.post_id)) {
postsMap.set(row.post_id, {
...row,
comments: []
})
}
if (row.comment_id) {
postsMap.get(row.post_id).comments.push({
comment_id: row.comment_id,
body: row.body,
created_at: row.comment_created_at
})
}
})
const posts = Array.from(postsMap.values())
これでPrismaの制約を回避しつつ、必要なデータを1回のクエリで取得できた。
デメリット
保守性の低下
- 検索条件やソートを追加するとSQLがすぐ長くなる
- スキーマ変更時の影響範囲が分かりにくい
型安全性の低下
- Prismaの恩恵を受けづらく、スキーマ変更に弱い
- 手動での型定義が必要
まとめ
- Prismaの
includeで空配列問題に直面 - 二段クエリではメモリを消費しすぎて限界
-
prisma.$queryRawUnsafeで1クエリ + アプリ側でのマッピングで解決
ただし保守性と型安全を犠牲にするため、パフォーマンスが求められる箇所に限定して使うのが現実解。