以下の内容はhttps://kazuhira-r.hatenablog.com/entry/2024/09/15/202800より取得しました。


Drizzle ORM(Drizzle Kit)で、既存のテーブル定義からスキーマを生成する

これは、なにをしたくて書いたもの?

前にDrizzle ORMを試してみました。

TypeScriptのORM、Drizzle ORMをMySQLで試す - CLOVER🍀

Drizzle ORMを使う時にはスキーマschema.ts)を作る必要があるのですが、これを既存のテーブル定義から作成してみたいと思います。

Drizzle Kitのintrospect(pull)コマンド

Drizzle Kitのintrospectコマンドを使うと、既存のテーブル定義からschema.tsを生成することができます。

せっかくなので、今回は前回のエントリーで作成したテーブル定義からDrizzle Kitのintrospectコマンドでschema.tsを生成してどうなるか
試してみましょう。

TypeScriptのORM、Drizzle ORMをMySQLで試す - CLOVER🍀

環境

今回の環境はこちら。

$ node --version
v20.17.0


$ npm --version
10.8.2

データベースにはMySQLを使い、172.17.0.2で動作しているものとします。

 MySQL  localhost:33060+ ssl  practice  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.4.2     |
+-----------+
1 row in set (0.0003 sec)

テーブル定義

この記事で使ったschema.tsです。

src/schema.ts

import { relations } from 'drizzle-orm';
import { int, mysqlTable, text, varchar } from 'drizzle-orm/mysql-core';

export const user = mysqlTable('user', {
  id: int('id').autoincrement().primaryKey(),
  firstName: varchar('first_name', { length: 10 }),
  lastName: varchar('last_name', { length: 10 }),
  age: int('age'),
});

export const userRelation = relations(user, ({ many }) => ({
  posts: many(post),
}));

export const post = mysqlTable('post', {
  id: int('id').autoincrement().primaryKey(),
  title: varchar('title', { length: 255 }),
  url: text('url'),
  userId: int('user_id').references(() => user.id),
});

export const postRelation = relations(post, ({ one }) => ({
  user: one(user, {
    fields: [post.userId],
    references: [user.id],
  }),
}));

TypeScriptのORM、Drizzle ORMをMySQLで試す - CLOVER🍀

マイグレーションで生成されたDDLはこちらです。
※実際には2ファイルです。

CREATE TABLE `user` (
        `id` int AUTO_INCREMENT NOT NULL,
        `first_name` varchar(10),
        `last_name` varchar(10),
        `age` int,
        CONSTRAINT `user_id` PRIMARY KEY(`id`)
);
CREATE TABLE `post` (
        `id` int AUTO_INCREMENT NOT NULL,
        `title` varchar(255),
        `url` text,
        `user_id` int,
        CONSTRAINT `post_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
ALTER TABLE `post` ADD CONSTRAINT `post_user_id_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE no action ON UPDATE no action;

データベースへの適用まで済ませておきます。

$ npx drizzle-kit migrate

テーブルが作成されました。

 MySQL  localhost:33060+ ssl  practice  SQL > show tables;
+----------------------+
| Tables_in_practice   |
+----------------------+
| __drizzle_migrations |
| post                 |
| user                 |
+----------------------+
3 rows in set (0.0016 sec)

この結果をDrizzle Kitのintrospectコマンドで取り込んでみます。

Node.jsプロジェクトを作成する

今回のお題向けに、新しくNode.jsプロジェクトを作成します。

$ npm init -y
$ npm i -D typescript
$ npm i -D @types/node@v20
$ npm i -D prettier
$ npm i -D vitest

ECMAScript Modulesとします。

  "type": "module",

現時点での依存関係。

  "devDependencies": {
    "@types/node": "^20.16.5",
    "prettier": "^3.3.3",
    "typescript": "^5.6.2",
    "vitest": "^2.1.1"
  }

scriptsの設定。

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "typecheck": "tsc --project ./tsconfig.typecheck.json",
    "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch",
    "test": "vitest run",
    "test:watch": "vitest watch",
    "format": "prettier --write src test"
  },

設定ファイル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": [
    "src/**/*"
  ]
}

tsconfig.typecheck.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src/**/*", "test/**/*"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 100
}

vite.config.ts

/// <reference types="vitest" />
import { defineConfig } from 'vite';

export default defineConfig({
  test: {
    environment: 'node',
    poolOptions: {
      forks: {
        singleFork: true,
      },
    },
  },
});

ソースコードsrcに、テストコードはtestに置くことにします。

$ mkdir src test

Drizzle Kitを使って、既存のテーブル定義を取り込む

では、Drizzle Kitを使って既存のテーブル定義を取り込んでみましょう。

まずはDrizzle ORM、Drizzle Kit、MySQLに接続するためのドライバーをインストール。

$ npm i drizzle-orm mysql2
$ npm i -D drizzle-kit

依存関係はこのようになりました。

  "devDependencies": {
    "@types/node": "^20.16.5",
    "drizzle-kit": "^0.24.2",
    "prettier": "^3.3.3",
    "typescript": "^5.6.2",
    "vitest": "^2.1.1"
  },
  "dependencies": {
    "drizzle-orm": "^0.33.0",
    "mysql2": "^3.11.2"
  }

Drizzle Kitの設定ファイルを作成。

drizzle.config.ts

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  dialect: 'mysql',
  schema: './src/schema.ts',
  out: './drizzle',
  dbCredentials: {
    host: '172.17.0.2',
    port: 3306,
    database: 'practice',
    user: 'kazuhira',
    password: 'password',
  },
});

Drizzle Kitのintrospectを実行してみます。

$ npx drizzle-kit introspect

なお、別名としてpullと指定することもできます。

$ npx drizzle-kit pull

コマンドの実行結果。

[✓] 2 tables fetched
[✓] 8 columns fetched
[✓] 1 indexes fetched
[✓] 1 foreign keys fetched

[✓] Your SQL migration file ➜ drizzle/0000_rainy_tenebrous.sql 🚀
[✓] You schema file is ready ➜ drizzle/schema.ts 🚀
[✓] You relations file is ready ➜ drizzle/relations.ts 🚀

Drizzle Kitのマイグレーション結果を管理するテーブルである、__drizzle_migrationsについてはスキップされているようですね。

生成されたディレクトリおよびファイル。

$ tree drizzle
drizzle
├── 0000_rainy_tenebrous.sql
├── meta
│   ├── 0000_snapshot.json
│   └── _journal.json
├── relations.ts
└── schema.ts

1 directory, 5 files

schema.tsはこちらの内容が生成されました。

drizzle/schema.ts

import { mysqlTable, mysqlSchema, AnyMySqlColumn, foreignKey, primaryKey, int, varchar, text } from "drizzle-orm/mysql-core"
import { sql } from "drizzle-orm"

export const post = mysqlTable("post", {
        id: int("id").autoincrement().notNull(),
        title: varchar("title", { length: 255 }),
        url: text("url"),
        userId: int("user_id").references(() => user.id),
},
(table) => {
        return {
                postId: primaryKey({ columns: [table.id], name: "post_id"}),
        }
});

export const user = mysqlTable("user", {
        id: int("id").autoincrement().notNull(),
        firstName: varchar("first_name", { length: 10 }),
        lastName: varchar("last_name", { length: 10 }),
        age: int("age"),
},
(table) => {
        return {
                userId: primaryKey({ columns: [table.id], name: "user_id"}),
        }
});

relationsがないぞ?と思いきや、別ファイルになっています。

drizzle/relations.ts

import { relations } from "drizzle-orm/relations";
import { user, post } from "./schema";

export const postRelations = relations(post, ({one}) => ({
        user: one(user, {
                fields: [post.userId],
                references: [user.id]
        }),
}));

export const userRelations = relations(user, ({many}) => ({
        posts: many(post),
}));

今回扱っているテーブルを作成するのに書いた、オリジナルのschema.tsはこちらでした。

src/schema.ts

import { relations } from 'drizzle-orm';
import { int, mysqlTable, text, varchar } from 'drizzle-orm/mysql-core';

export const user = mysqlTable('user', {
  id: int('id').autoincrement().primaryKey(),
  firstName: varchar('first_name', { length: 10 }),
  lastName: varchar('last_name', { length: 10 }),
  age: int('age'),
});

export const userRelation = relations(user, ({ many }) => ({
  posts: many(post),
}));

export const post = mysqlTable('post', {
  id: int('id').autoincrement().primaryKey(),
  title: varchar('title', { length: 255 }),
  url: text('url'),
  userId: int('user_id').references(() => user.id),
});

export const postRelation = relations(post, ({ one }) => ({
  user: one(user, {
    fields: [post.userId],
    references: [user.id],
  }),
}));

すごいですね、ほぼ同じ内容になっています。

出力先はoutに指定された方が使われるんですね。schemaの方が上書きされるのかなと思っていました…。

  schema: './src/schema.ts',
  out: './drizzle',

ちなみにマイグレーションファイルもできています。

drizzle/0000_rainy_tenebrous.sql

-- Current sql file was generated after introspecting the database
-- If you want to run this migration please uncomment this code before executing migrations
/*
CREATE TABLE `post` (
        `id` int AUTO_INCREMENT NOT NULL,
        `title` varchar(255),
        `url` text,
        `user_id` int,
        CONSTRAINT `post_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
CREATE TABLE `user` (
        `id` int AUTO_INCREMENT NOT NULL,
        `first_name` varchar(10),
        `last_name` varchar(10),
        `age` int,
        CONSTRAINT `user_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
ALTER TABLE `post` ADD CONSTRAINT `post_user_id_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE no action ON UPDATE no action;

このファイルはsrc配下にコピーしておきます。

$ cp drizzle/{schema.ts,relations.ts} src

動作確認のために、こちらで書いたテストコードを動かしてみます。

TypeScriptのORM、Drizzle ORMをMySQLで試す - CLOVER🍀

ポイントはDrizzle ORMの設定をしているところなはずなので、こう書いていた部分を

import * as schema from '../src/schema.js';
import { post, user } from '../src/schema.js';
import { and, asc, eq, gt } from 'drizzle-orm';

const connection = mysql2.createConnection({
  host: '172.17.0.2',
  port: 3306,
  database: 'practice',
  user: 'kazuhira',
  password: 'password',
});

const db = drizzle(connection, { mode: 'default', schema });

こうすればいいのかなと思ったのですが(relations.tsを追加し、drizzleschemaに指定)

import * as schema from '../src/schema.js';
import * as relations from '../src/relations.js';
import { post, user } from '../src/schema.js';
import { and, asc, eq, gt } from 'drizzle-orm';

const connection = mysql2.createConnection({
  host: '172.17.0.2',
  port: 3306,
  database: 'practice',
  user: 'kazuhira',
  password: 'password',
});

const db = drizzle(connection, { mode: 'default', schema: { ...schema, ...relations } });

それだけではうまく動きませんでした。

なにがダメだったかというと、この部分がうまく動きません。auto incrementの結果を受け取るところで、値が取れなくなりました。

    // データ登録
    const katsuoId = await tx
      .insert(user)
      .values({ firstName: 'カツオ', lastName: '磯野', age: 11 })
      .$returningId(); // auto incrementの結果を取得

これを修正するには、schema.tsの以下の定義を

export const user = mysqlTable("user", {
    id: int("id").autoincrement().notNull(),
    firstName: varchar("first_name", { length: 10 }),
    lastName: varchar("last_name", { length: 10 }),
    age: int("age"),
},
(table) => {
    return {
        userId: primaryKey({ columns: [table.id], name: "user_id"}),
    }
});

こう変える必要がありました(idprimaryKeyを明示的に指定)。

export const user = mysqlTable("user", {
    id: int("id").autoincrement().notNull().primaryKey(),
    firstName: varchar("first_name", { length: 10 }),
    lastName: varchar("last_name", { length: 10 }),
    age: int("age"),
},
(table) => {
    return {
        userId: primaryKey({ columns: [table.id], name: "user_id"}),
    }
});

あとはrelations.tsimport文が拡張子なしで出力されるので、ECMAScript Modules形式を使っていると怒られます…。

import { user, post } from "./schema";

とはいえ、修正したのはそれくらいだったのでこの使い方でも大丈夫そうですね。

テーブル定義を変更して、もう1度introspectしてみる

テーブル定義を変更してもう1度introspectするとどうなるか試してみましょう。

こんなDDLを実行してみます。

create table family(
  id int auto_increment,
  name varchar(10),
  primary key(id)
);

alter table user add column family_id int;
alter table user add constraint foreign key(family_id) references family(id);

テーブルをひとつ追加して、既存のテーブルにカラムと外部キーを作成。

もう1度introspectしてみます。

$ npx drizzle-kit introspect

結果。

[✓] 3  tables fetched
[✓] 11 columns fetched
[✓] 2  indexes fetched
[✓] 2  foreign keys fetched

[i] No SQL generated, you already have migrations in project
[✓] You schema file is ready ➜ drizzle/schema.ts 🚀
[✓] You relations file is ready ➜ drizzle/relations.ts 🚀

SQLファイルはもう作られないようですね。生成済みのSQLファイルが上書きされるかというと、そんなこともありません。
SQLファイルを削除しても生成されません(!)。

スキーマについては上書きして生成されているので確認してみます。

drizzle/schema.ts

import { mysqlTable, mysqlSchema, AnyMySqlColumn, primaryKey, int, varchar, foreignKey, text, index } from "drizzle-orm/mysql-core"
import { sql } from "drizzle-orm"

export const family = mysqlTable("family", {
        id: int("id").autoincrement().notNull(),
        name: varchar("name", { length: 10 }),
},
(table) => {
        return {
                familyId: primaryKey({ columns: [table.id], name: "family_id"}),
        }
});

export const post = mysqlTable("post", {
        id: int("id").autoincrement().notNull(),
        title: varchar("title", { length: 255 }),
        url: text("url"),
        userId: int("user_id").references(() => user.id),
},
(table) => {
        return {
                postId: primaryKey({ columns: [table.id], name: "post_id"}),
        }
});

export const user = mysqlTable("user", {
        id: int("id").autoincrement().notNull(),
        firstName: varchar("first_name", { length: 10 }),
        lastName: varchar("last_name", { length: 10 }),
        age: int("age"),
        familyId: int("family_id").references(() => family.id),
},
(table) => {
        return {
                familyId: index("family_id").on(table.familyId),
                userId: primaryKey({ columns: [table.id], name: "user_id"}),
        }
});

drizzle/relations.ts

import { relations } from "drizzle-orm/relations";
import { user, post, family } from "./schema";

export const postRelations = relations(post, ({one}) => ({
        user: one(user, {
                fields: [post.userId],
                references: [user.id]
        }),
}));

export const userRelations = relations(user, ({one, many}) => ({
        posts: many(post),
        family: one(family, {
                fields: [user.familyId],
                references: [family.id]
        }),
}));

export const familyRelations = relations(family, ({many}) => ({
        users: many(user),
}));

こちらにはしっかり反映されているようです。

introspectするテーブルを絞り込む

introspectする対象のテーブルを絞り込むには、tablesFilterを使うようです。

Configuring Drizzle kit / Configuration / tablesFilters

たとえば以下のようにtablesFilterを定義すると、introspectスキーマに反映されるのはuserpostの2つのテーブルに絞り込まれます。

drizzle.config.ts

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  dialect: 'mysql',
  schema: './src/schema.ts',
  out: './drizzle',
  tablesFilter: ['user', 'post'],
  dbCredentials: {
    host: '172.17.0.2',
    port: 3306,
    database: 'practice',
    user: 'kazuhira',
    password: 'password',
  },
});

この設定自体はintrospectだけでなく、Drizzle Kitからスキーマ定義をデータベースに反映するフローでも使えるようです(むしろそちらが
主体の使い方だと思いますが)。

おわりに

Drizzle ORM(Drizzle Kit)で、既存のテーブル定義からスキーマを生成してみました。

前回のエントリーの結果と完全に同じになったわけではないですが、ほぼそのまま使える結果が生成されたように思います。
便利ですね。

他のアプリケーションが同じデータベースを見ているなどで、テーブル定義をDrizzle ORMおよびDrizzle Kitに任せたくない場合はこういった
方法もありなのかなと思います。




以上の内容はhttps://kazuhira-r.hatenablog.com/entry/2024/09/15/202800より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14