0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PrismaORMで1:Nリレーションが空配列になった時にSQL直書きで対応した話

0
Posted at

困ったこと

一覧画面で投稿とコメントを表示する際、Prismaのincludeでリレーション先に条件を付けて取得しようとしたが、期待通りにフィルタされた子レコードが取得できず、空配列が返される問題が発生した。

具体的には以下のような一覧取得を実現したかった:

  • 公開済みの投稿一覧
  • 各投稿に対して削除されていないコメントのみ
  • コメントは作成日時の降順でソート
// Prismaで理想的だった処理(うまくいかなかった)
const posts = await prisma.post.findMany({
  where: { published: true },
  include: {
    comments: {
      where: { isDeleted: false },
      orderBy: { createdAt: 'desc' },
    },
  },
})

しかし実際には、includeの条件がうまく効かずコメントが空配列で返されることが多発。

一時的な回避策

仕方なく以下の手順で実装していた:

  1. 投稿データを先に取得
  2. 取得した投稿のID一覧でコメントを再取得
  3. サーバー側でフィルタリングとソート処理
  4. アプリ側で手動マージ
// 実際に使っていた二段クエリでの回避策
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の恩恵を受けづらく、スキーマ変更に弱い
  • 手動での型定義が必要

まとめ

  1. Prismaのincludeで空配列問題に直面
  2. 二段クエリではメモリを消費しすぎて限界
  3. prisma.$queryRawUnsafeで1クエリ + アプリ側でのマッピングで解決

ただし保守性と型安全を犠牲にするため、パフォーマンスが求められる箇所に限定して使うのが現実解。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?