TypeScriptプロジェクトでORMを選定する際、型安全性とパフォーマンスは重要なポイントになります。Drizzle ORMはこれらの要件を満たす選択肢の一つであり、特にrelations
機能は、複雑なテーブル間の関連を型安全かつ宣言的に扱う上で非常に有効です。
多くのアプリケーションでは、データが複数のテーブルに正規化されて保存されています。これらの関連データを取得する際、手動で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 ORMquery
メソッド (Eager loading)