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

:::note
【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` 多機能だから抑えるの大変だね！
今日はリレーション系。

https://orm.drizzle.team/docs/relations


## その前に Debug モード

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

```ts: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.ts` は `drizzle-kit` のみで使われるらしい。
> コード内で実行するときは利用されないので、何か手法を考えてみたい。


## Soft Relation

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

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

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

```ts: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 は付与されていない。
> 
> ```sql: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 と同期しておく。

```bash
$ npx drizzle-kit generate
$ npx drizzle-kit push
```




## リレーションの取得方法

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/d9509fd4-37bd-4a05-831a-772748476d36.png)

```tsx: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` とすればよい。

```ts
with: {
  posts: true,
},
```

```json
"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` などやりたいことは一通りできそう。
`map` や `filter` を駆使して、`with` を実現することもできる。

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

```ts
select({
  ...getTableColumns(posts),
  author: users.name,
})
```

```json
"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()` というメソッドを利用する。

```diff_tsx: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` を行うと、ちゃんとリレーションが付く。

```bash
$ npx drizzle-kit generate
$ npx drizzle-kit push
```

> Drizzle はいったん削除して、データを置き換える形でマイグレーションを行うみたい。
> `ALTER` で実装すると汚くなるから良い実装かもしれない。
>
> ```sql: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は張ろう）

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