3
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?

Drizzle ORM でリレーションを張る方法

Last updated at Posted at 2025-03-23

【Drizzle ORM シリーズ】
・Drizzle ORM を Next.js で使ってみる #Next.js - Qiita
 https://qiita.com/sigeta/items/c0a2b0463eca06314906
・Drizzle ORM でスキーマファイルを使ってDBを操作する方法 #Next.js - Qiita
 https://qiita.com/sigeta/items/99a625c17c6a0a75da54
・Drizzle ORM でリレーションを張る方法 #Next.js - Qiita
 https://qiita.com/sigeta/items/b800f3e38beb1639d2b7
・Drizzle ORM の設定ファイルを接続時も利用する方法 #TypeScript - Qiita
 https://qiita.com/sigeta/items/d9267a06061d4d2a51e8
・Drizzle ORM でダミーデータを流し込む方法 #TypeScript - Qiita
 https://qiita.com/sigeta/items/8bc85b74273eeb37c27a

Drizzle ORM 多機能だから抑えるの大変だね!
今日はリレーション系。

その前に Debug モード

DB接続時の設定で logger: true を指定すると、発行SQLがコンソールに出力される。
便利なので、開発時は有効にしても良いかもしれない。

db/index.ts
import { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import * as schema from './schema'
import process from 'node:process'

process.loadEnvFile()

const sqlite = new Database(process.env.DATABASE_URL!)
export const db: BetterSQLite3Database<typeof schema> = drizzle(sqlite, {
  schema,
  casing: 'snake_case',
  logger: true, // SQLログ出力
})

drizzle.config.tsdrizzle-kit のみで使われるらしい。
コード内で実行するときは利用されないので、何か手法を考えてみたい。

Soft Relation

あまり聞きなれない用語だが、要するに外部キーを使わないリレーション。
これはDB層ではなく、アプリケーション層でリレーションを構築することで柔軟に運用できる。
既存の大きいDBをラップしたり、リレーションに対応していないDBも扱えそう。

一対一、一対多、多対多に対応している。

前の記事で作った users に posts を追加する。
一人(user)は複数の記事(post)を持つ。

db/schema.ts
import { relations, sql } from 'drizzle-orm'
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'

export const users = sqliteTable('users', {
  id: integer().notNull().primaryKey(),
  name: text().notNull(),
  age: integer(),

  createdAt: integer({ mode: 'timestamp' }).notNull()
    .default(sql`(unixepoch())`),
  updatedAt: integer({ mode: 'timestamp' }).notNull()
    .default(sql`(unixepoch())`)
    .$onUpdate(() => sql`(unixepoch())`),
})

export const posts = sqliteTable('posts', {
  id: integer().notNull().primaryKey(),
  userId: integer().notNull(), // Foreign Key
  title: text().notNull(),
  body: text(),

  createdAt: integer({ mode: 'timestamp' }).notNull()
    .default(sql`(unixepoch())`),
  updatedAt: integer({ mode: 'timestamp' }).notNull()
    .default(sql`(unixepoch())`)
    .$onUpdate(() => sql`(unixepoch())`),
})

// リレーションの定義
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}))

export const postsRelations = relations(posts, ({ one }) => ({
  user: one(users, {
    fields: [posts.userId],
    references: [users.id],
  }),
}))

こんな感じで relatioins を使って双方向に定義するみたい。

userId: integer().notNull(), // Foreign Key と書いているが、
drizzle/sql を確認すると、Foreign Key は付与されていない。

db/drizzle/0001_same_captain_cross.sql
CREATE TABLE `posts` (
	`id` integer PRIMARY KEY NOT NULL,
	`user_id` integer NOT NULL,
	`title` text NOT NULL,
	`body` text,
	`created_at` integer DEFAULT (unixepoch()) NOT NULL,
	`updated_at` integer DEFAULT (unixepoch()) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `users` (
	`id` integer PRIMARY KEY NOT NULL,
	`name` text NOT NULL,
	`age` integer,
	`created_at` integer DEFAULT (unixepoch()) NOT NULL,
	`updated_at` integer DEFAULT (unixepoch()) NOT NULL
);

作ったら DB と同期しておく。

$ npx drizzle-kit generate
$ npx drizzle-kit push

リレーションの取得方法

ではこのデータを取得してみる。

ChatGPT くんに適当にテストデータを作ってもらった。
それを npx drizzle-orm studio で登録する。

image.png

lib/repository.tsx
'use server'

import { db } from '@/db'
import { posts, users } from '@/db/schema'
import { asc, desc, eq, like, and } from 'drizzle-orm'

export const getUsers = async () => {
  const resultA = await db.query.users
    .findMany({
      where: and(
        like(users.name, '%中%'),
      ),
      orderBy: asc(users.id),
      with: {
        posts: {
          where: like(posts.title, '%の%'),
          orderBy: desc(posts.id),
        },
      },
    })

  const resultB = (
    await db.select()
      .from(posts)
      .leftJoin(users, eq(users.id, posts.userId))
      .where(and(
        like(users.name, '%中%'),
        like(posts.title, '%の%')
      ))
      .orderBy(asc(users.id))
    )

  return {
    resultA,
    resultB,
  }
}

resultA

結果はこんな感じ!

resultA はいわゆるビルダー形式での検索。
json クエリを多用して、一回のSQL発行で取得していた。
(Laravel などとは異なるみたい)

ちなみに with に制約を設けないのであれば posts: true とすればよい。

with: {
  posts: true,
},
"resultA": [
{
  "id": 1,
  "name": "中島",
  "age": 27,
  "createdAt": "2025-03-23T05:32:58.000Z",
  "updatedAt": "2025-03-23T05:32:58.000Z",
  "posts": [
    {
      "id": 10,
      "userId": 1,
      "title": "雨の日の楽しみ方",
      "body": "雨の日はお気に入りのレインコートを着て、静かなカフェで過ごすのが最近のマイブーム。",
      "createdAt": "2025-03-23T05:47:46.000Z",
      "updatedAt": "2025-03-23T05:47:46.000Z"
    },

resultB

resultB は素のSQL形式での検索。
leftJoin などやりたいことは一通りできそう。
mapfilter を駆使して、with を実現することもできる。

デフォルトだと、モデルごとにまとめて json に加工してくれる。
これが面倒な場合は、必要なカラムを手動で指定できる。
ちなみに select にて posts のカラムだけ取得する場合は、短縮メソッドが用意されている。

select({
  ...getTableColumns(posts),
  author: users.name,
})
"resultB": [
{
  "posts": {
    "id": 1,
    "userId": 1,
    "title": "春の訪れとともに",
    "body": "今日は近くの公園に行ったら、桜が咲き始めていました。まだ五分咲きでしたが、春が近づいてきているのを実感しました。",
    "createdAt": "2025-03-23T05:47:46.000Z",
    "updatedAt": "2025-03-23T05:47:46.000Z"
  },
  "users": {
    "id": 1,
    "name": "中島",
    "age": 27,
    "createdAt": "2025-03-23T05:32:58.000Z",
    "updatedAt": "2025-03-23T05:32:58.000Z"
  }
},

Relation

DB に ForeignKey のリレーションを付与する、よくあるやつ。
これは references() というメソッドを利用する。

db/schema.ts
  export const posts = sqliteTable('posts', {
    id: integer().notNull().primaryKey(),
-   userId: integer().notNull(), // Foreign Key
+   userId: integer().notNull().references(() => users.id), // Foreign Key
    title: text().notNull(),

とても簡単。
この状態で drizzle-kit generate を行うと、ちゃんとリレーションが付く。

$ npx drizzle-kit generate
$ npx drizzle-kit push

Drizzle はいったん削除して、データを置き換える形でマイグレーションを行うみたい。
ALTER で実装すると汚くなるから良い実装かもしれない。

db/drizzle/0002_needy_pestilence.sql
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_posts` (
	`id` integer PRIMARY KEY NOT NULL,
	`user_id` integer NOT NULL,
	`title` text NOT NULL,
	`body` text,
	`created_at` integer DEFAULT (unixepoch()) NOT NULL,
	`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
	FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
INSERT INTO `__new_posts`("id", "user_id", "title", "body", "created_at", "updated_at") SELECT "id", "user_id", "title", "body", "created_at", "updated_at" FROM `posts`;--> statement-breakpoint
DROP TABLE `posts`;--> statement-breakpoint
ALTER TABLE `__new_posts` RENAME TO `posts`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

データの取得などは先ほどと同様。
安全に使うのならば Foreign Key を指定したほうが良いが、丁寧に実装しないといけないのでケースバイケースにはなると思う。
(大昔使わずにシステムを作成したが、特に大きな影響は出なかった。indexは張ろう)

続き
Drizzle ORM の設定ファイルを接続時も利用する方法 #TypeScript - Qiita
https://qiita.com/sigeta/items/d9267a06061d4d2a51e8

3
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
3
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?