これは、なにをしたくて書いたもの?
前に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
を追加し、drizzle
のschema
に指定)
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"}), } });
こう変える必要がありました(id
にprimaryKey
を明示的に指定)。
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.ts
のimport
文が拡張子なしで出力されるので、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
でスキーマに反映されるのはuser
とpost
の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に任せたくない場合はこういった
方法もありなのかなと思います。