【TypeScript】Drizzle ORMのリレーション実装方法

【TypeScript】Drizzle ORMのリレーション実装方法

本記事では、Drizzle ORMのrelations機能を用いた具体的なスキーマ設計とクエリの実装例について、私自身が得た技術的な知見を共有します。

Drizzle #Typescript#Drizzle#ORM

【TypeScript】Drizzle ORMのリレーション実装方法

サムネイル

本記事では、Drizzle ORMのrelations機能を用いた具体的なスキーマ設計とクエリの実装例について、私自身が得た技術的な知見を共有します。

更新日: 7/8/2025

TypeScriptプロジェクトでORMを選定する際、型安全性とパフォーマンスは重要なポイントになります。Drizzle ORMはこれらの要件を満たす選択肢の一つであり、特にrelations機能は、複雑なテーブル間の関連を型安全かつ宣言的に扱う上で非常に有効です。

ISSUE - 課題

多くのアプリケーションでは、データが複数のテーブルに正規化されて保存されています。これらの関連データを取得する際、手動でSQLのJOIN句を記述する方法では、クエリが複雑化しやすくバグの原因になりやすい箇所です。

本件に関して、ほかのORMと同様にDrizzle ORMで対応できるので説明していきます。

基本的なリレーションの定義方法

ここでは、一般的な構成である「ユーザー」「投稿」「カテゴリ」を例に、リレーションの定義方法を解説します。定義する関係性は以下の通りです。

From Table Relationship To Table 備考
users 一対多 (One-to-Many) posts 一人のユーザーが複数の投稿を持つ
posts 多対一 (Many-to-One) users 一つの投稿は一人のユーザーに属する
posts 多対多 (Many-to-Many) categories 中間テーブル (postsToCategories) を経由

これを表現するためのスキーマ定義がこちらです。

db/schema.ts

import { sqliteTable, integer, text, primaryKey } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm/relations';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
});

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  authorId: integer('author_id').notNull().references(() => users.id),
});

export const categories = sqliteTable('categories', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
});

export const postsToCategories = sqliteTable('posts_to_categories', {
  postId: integer('post_id').notNull().references(() => posts.id),
  categoryId: integer('category_id').notNull().references(() => categories.id),
}, (table) => ({
  pkey: primaryKey({ columns: [table.postId, table.categoryId] }),
}));

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],
  }),
  postsToCategories: many(postsToCategories),
}));

export const categoriesRelations = relations(categories, ({ many }) => ({
  postsToCategories: many(postsToCategories),
}));

export const postsToCategoriesRelations = relations(postsToCategories, ({ one }) => ({
  post: one(posts, {
    fields: [postsToCategories.postId],
    references: [posts.id],
  }),
  category: one(categories, {
    fields: [postsToCategories.categoryId],
    references: [categories.id],
  }),
}));

with句を使ったデータの取得

リレーションを定義すると、db.queryオブジェクトのwithプロパティを使い、シンプルな記述で関連データを取得できるようになります。
例えば、「IDが1のユーザーが投稿した、カテゴリ情報を含む全投稿」を取得するコードは以下のようになります。

import { db } from './db';

const userWithPostsAndCategories = await db.query.users.findFirst({
  where: (users, { eq }) => eq(users.id, 1),
  with: {
    posts: {
      with: {
        postsToCategories: {
          with: {
            category: true,
          },
        },
      },
    },
  },
});

withプロパティをネストさせることで、関連するテーブルの情報を宣言的に取得できるのは、Drizzle ORMの非常に強力な点だと感じています。

【応用編】relationName

より複雑なケースとして、一つのマスターテーブルが、複数の異なるテーブルの「種別」を管理する場合があります。

例えば、「コンテンツ種別 (contentTypes)」というテーブルがあり、これが「記事 (articles)」と「まとめ (summaries)」の両方に対する種別情報を持つ、といったケースです。この曖昧さを解決するのがrelationNameです。

定義する関係性は以下の通りで、relationNameを使い2つの異なる関係性を明確に区別します。

From Table Relationship To Table relationName
contentTypes 一対多 articles 'contentType_articles'
contentTypes 一対多 summaries 'contentType_summaries'
articles 多対一 contentTypes 'contentType_articles'
summaries 多対一 contentTypes 'contentType_summaries'

この関係性をコードで表現します。

import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm/relations';

export const contentTypes = sqliteTable('content_types', {
  id: integer('id').primaryKey(),
  name: text('name').notNull(),
});

export const articles = sqliteTable('articles', {
  id: integer('id').primaryKey(),
  title: text('title').notNull(),
  contentTypeId: integer('content_type_id').notNull().references(() => contentTypes.id),
});

export const summaries = sqliteTable('summaries', {
  id: integer('id').primaryKey(),
  title: text('title').notNull(),
  contentTypeId: integer('content_type_id').notNull().references(() => contentTypes.id),
});

export const contentTypesRelations = relations(contentTypes, ({ many }) => ({
  articles: many(articles, {
    relationName: 'contentType_articles',
  }),
  summaries: many(summaries, {
    relationName: 'contentType_summaries',
  }),
}));

export const articlesRelations = relations(articles, ({ one }) => ({
  contentType: one(contentTypes, {
    fields: [articles.contentTypeId],
    references: [contentTypes.id],
    relationName: 'contentType_articles',
  }),
}));

export const summariesRelations = relations(summaries, ({ one }) => ({
  contentType: one(contentTypes, {
    fields: [summaries.contentTypeId],
    references: [contentTypes.id],
    relationName: 'contentType_summaries',
  }),
}));

contentTypesRelations側で定義したrelationNameと、articlesRelations / summariesRelations側で定義したrelationNameを対応させることで、Drizzleに2つの異なる関連を正しくペアとして認識させることができます。

まとめ

Drizzle ORMのrelations機能は、データアクセスに関するコードの可読性と保守性を向上させる上で、非常に有効なツールであると実感しました。SQLのJOINを意識することなくオブジェクトのように直感的にクエリを組み立てられる点に加え、取得データが完全に型付けされる安全性など、typescriptを使う技術者にとっては嬉しいポイントが多めです・

より詳しい情報については、以下の公式ドキュメントを参照してください。
Drizzle ORM 公式ドキュメント (Relations)
Drizzle ORM queryメソッド (Eager loading)

検索

検索条件に一致する記事が見つかりませんでした