Drizzle ORMのリレーションでテーブルを結合して取得した話

Drizzle ORMの relations を使うと、テーブル間の関係を定義してシンプルなコードで結合データを取得できます。postscomments を例に実装しました。


スキーマにリレーションを定義する

// src/lib/db/schema.ts
import { int, text, sqliteTable } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';

export const posts = sqliteTable('posts', {
  id: int('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  body: text('body').notNull(),
});

export const comments = sqliteTable('comments', {
  id: int('id').primaryKey({ autoIncrement: true }),
  postId: int('post_id').notNull(),
  body: text('body').notNull(),
  createdAt: int('created_at').notNull(),
});

// postsは複数のcommentsを持つ
export const postsRelations = relations(posts, ({ many }) => ({
  comments: many(comments),
}));

// commentsはひとつのpostに属する
export const commentsRelations = relations(comments, ({ one }) => ({
  post: one(posts, {
    fields: [comments.postId],    // comments側の外部キー
    references: [posts.id],        // posts側の参照先
  }),
}));

manyone の使い分け

意味 今回の例
many 1対多の「多」側 1つの post は複数の comments を持つ
one 多対1の「1」側 1つの comment は1つの post に属する

fieldsreferences

one(posts, {
  fields: [comments.postId],    // comments側の外部キー
  references: [posts.id],        // posts側の参照先
})

SQLで書くと JOIN posts ON comments.post_id = posts.id と同じ意味です。


Query APIでリレーションを使う

全件取得(with)

const db = createDb(platform!.env.DB);

const posts = await db.query.posts.findMany({
  with: { comments: true },
});
// → [{ id: 1, title: '...', comments: [{ id: 1, body: '...' }, ...] }, ...]

findManyfindFirst

意味
findMany 条件に合う全件取得(配列を返す)
findFirst 条件に合う最初の1件取得

条件付きクエリ

// 特定のpostとそのcomments
const post = await db.query.posts.findFirst({
  where: (posts, { eq }) => eq(posts.id, 1),
  with: { comments: true },
});

// commentsに条件・並び替えを指定
const postsWithComments = await db.query.posts.findMany({
  with: {
    comments: {
      where: (comments, { eq }) => eq(comments.postId, 1),
      orderBy: (comments, { desc }) => [desc(comments.createdAt)],
    },
  },
});

where で使えるヘルパー関数

ヘルパー SQLの対応
eq(col, val) col = val
ne(col, val) col != val
gt(col, val) col > val
lt(col, val) col < val
like(col, val) col LIKE val
and(...条件) 条件 AND 条件
or(...条件) 条件 OR 条件

SvelteKitへの組み込み例

// src/routes/posts-with-comments/+page.server.ts
import type { PageServerLoad } from './$types';
import { createDb } from '$lib/db';

export const load: PageServerLoad = async ({ platform }) => {
  const db = createDb(platform!.env.DB);

  const posts = await db.query.posts.findMany({
    with: { comments: true },
  });

  return { posts };
};
<!-- src/routes/posts-with-comments/+page.svelte -->
<script lang="ts">
  let { data } = $props();
</script>

{#each data.posts as post}
  <h2>{post.title}</h2>
  <p>{post.body}</p>

  <h3>コメント({post.comments.length}件)</h3>
  <ul>
    {#each post.comments as comment}
      <li>{comment.body}</li>
    {/each}
  </ul>
{/each}

内部で発行されるSQL

Drizzleはリレーションを取得するとき、JOINではなく2クエリ方式を使います:

-- ① postsを全件取得
SELECT * FROM posts;

-- ② 取得したpost_idに対応するcommentsを取得
SELECT * FROM comments WHERE post_id IN (1, 2, 3, ...);

N+1問題を防ぎつつ、JOINより扱いやすい形でデータを返します。


Query API と select() の使い分け

// Query API(リレーションが使いやすい)
const posts = await db.query.posts.findMany({
  with: { comments: true },
});

// select API(SQLに近い書き方)
const posts = await db.select().from(posts);
Query API select API
リレーション with で簡単に取得 手動でJOINが必要
書き方 宣言的・直感的 SQLに近い
向いている場面 リレーションを含む複雑なデータ取得 シンプルなクエリ

まとめ

  • relations() でテーブル間の関係を定義する(many / one
  • fieldsreferences で外部キーと参照先を指定する
  • Query APIの findMany / findFirst + with でリレーションを含むデータを取得できる
  • 内部では2クエリ方式でN+1問題を防いでいる
← トップページに戻る