🐡

Drizzle Kitでデータベースマイグレーションを行う

2024/01/03に公開

Drizzle Kit

Drizzle KitはDrizzleKitは、DrizzleORM向けのCLIマイグレーションツールである。

具体的には以下のようにできる。

  • Typescriptのスキーマファイルをもとにマイグレーションファイルの作成。
  • DBにマイグレーションファイルの適用。
  • 既存のDBからTypescriptのスキーマファイルとマイグレーションファイルの作成。

今回はDrizzle Kitを使ってマイグレーションファイルを作成およびテーブルの作成を行う。

ディレクトリ構成

  • docker-compose.yml・Dockerfile:DBの設定を行う。
  • .env:DBの接続先を管理する。
  • drizzle.config.ts:DBの接続先を設定を行う。
  • schema.ts:スキーマの設定を行う。
├── drizzle
│   └── drizzle.config.ts
├── postgres
│   └── Dockerfile
├── src
│   └── infrastructure
│       └── config
│           └── schema.ts
├── .env
├── docker-compose.yml
└── package.json

※ schema.tsはGitで管理したい+Drizzle ORMで使用することがあるのでsrc配下に配置している。
※ drizzle.config.tsはGitで管理したくないのでdrizzle配下に配置している。

DBを用意する(docker-compose.yml・Dockerfile)

DBはDockerで用意する。

docker-compose.yml
version: '3.9'
services:
  postgres:
    container_name: postgres
    build:
      context: ./postgres
      dockerfile: Dockerfile
    environment:
      POSTGRES_USER: 'admin'
      POSTGRES_PASSWORD: 'password'
      POSTGRES_DB: 'db'
      POSTGRES_INITDB_ARGS: '--encoding=UTF8 --locale=C'
      PGDATA: /var/lib/postgresql/data/pgdata
      TZ: 'UTC'
    ports:
      - "5432:5432"
    volumes:
      - ./db/data:/var/lib/postgresql/data
      - ./db/initdb.d:/docker-entrypoint-initdb.d

  yamllint:
    container_name: aggregate_yamllint
    build:
      context: ./yamllint
      dockerfile: Dockerfile
    volumes:
      - .:/fastify
    entrypoint: /usr/bin/yamllint
    command: --help
FROM postgres:14

EXPOSE 5432

以下のコマンドでDBを起動する。

docker-compose up postgres

接続先の情報を.envファイルに追加しておく。

DATABASE_URL=postgresql://admin:password@localhost:5432/db?schema=public

DBの接続先を設定する(drizzle.config.ts)

drizzle-kitdotenvをインストールする。

npm install -D drizzle-kit
npm install dotenv --save

Configuring Drizzle kitを参考にdrizzle.config.tsの設定を行う。

drizzle.config.ts
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";

dotenv.config();

export default {
  schema: "./src/infrastructure/config/schema.ts",
  out: "./drizzle",
  driver: 'pg',
  dbCredentials: {
    connectionString: process.env.DATABASE_URL || "",
  }
} satisfies Config;
  • schema:スキーマファイルの配置場所を設定する。
  • out:Drizzle Kitが作成したSQLの出力先を設定する。
  • driver:ドライバー('pg' | 'mysql2' | 'better-sqlite' | 'libsql' | 'turso' | 'd1')
  • dbCredentials:DBの接続先を設定する。dotenvを使用して.envファイルから読み込む。

スキーマファイルを設定する(schema.ts)

schema.tsを設定するにあたって、Drizzle ORMをインストールする。

npm i drizzle-orm

SQL schema declarationを参考に以下のようなテーブルを作成するようにschema.tsを作成する。

  • userテーブル:姓と名を持つ。
  • contactテーブル:電話番号とメールアドレスを持つ。

user テーブル

カラム名 データ型 制約
id SERIAL PRIMARY KEY
created_at TIMESTAMP NOT NULL DEFAULT NOW()
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
deleted_at TIMESTAMP
first_name VARCHAR(256) NOT NULL
last_name VARCHAR(256) NOT NULL

contact テーブル

カラム名 データ型 制約
id SERIAL PRIMARY KEY
created_at TIMESTAMP NOT NULL DEFAULT NOW()
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
deleted_at TIMESTAMP
phone_number VARCHAR(20) NOT NULL
email VARCHAR(256) NOT NULL
user_id INTEGER REFERENCES user(id)
schema.ts
import { integer, pgTable, serial, timestamp, varchar } from 'drizzle-orm/pg-core';

const id = {
  id: serial('id').primaryKey(),
};
const timestamps = {
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
  deletedAt: timestamp('deleted_at'),
};
const schemaBase = {
  ...id,
  ...timestamps,
};

export const user = pgTable('user', {
  ...schemaBase,
  firstName: varchar('first_name', { length: 256 }).notNull(),
  lastName: varchar('last_name', { length: 256 }).notNull(),
});

export const contact = pgTable('contact', {
  ...schemaBase,
  phoneNumber: varchar('phone_number', { length: 20 }).notNull(),
  email: varchar('email', { length: 256 }).notNull(),
  userId: integer('user_id').references(() => user.id),
});

コマンドを設定する

List of commandsを参考に実行コマンドを設定していく。

Drizzle Kitには以下のコマンドが用意されている。

  • generate:schema.tsからマイグレーションファイルを作成する。
  • push:マイグレーションファイルをDBに適用する。
  • introspect:DBからマイグレーションファイルとschema.tsを作成する。

必要な設定はdrizzle.config.tsに設定済みなので、オプションで指定して実行する。

package.json
  "scripts": {
    "drizzle:generate": "drizzle-kit generate:pg --config=./drizzle/drizzle.config.ts",
    "drizzle:push": "drizzle-kit push:pg --config=./drizzle/drizzle.config.ts",
    "drizzle:introspect": "drizzle-kit introspect:pg --config=./drizzle/drizzle.config.ts",
    }

schema.tsからマイグレーションファイルを作成する(generate)

generateコマンドを実行する。

npm run drizzle:generate
実行結果
> fastify@1.0.0 drizzle:generate
> drizzle-kit generate:pg --config=./drizzle/drizzle.config.ts

drizzle-kit: v0.20.7
drizzle-orm: v0.29.1

Reading config file 'D:\program\fastify\drizzle\drizzle.config.ts'
2 tables
contact 7 columns 0 indexes 1 fks
user 6 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle\0000_superb_magneto.sql 🚀

drizzle配下にマイグレーションファイルが作成されている。

出力結果
└── drizzle
    ├── meta
    │   ├── _journal.json
    │   └── 0000_snapshot.json
    ├── 0000_superb_magneto.sql
    └── drizzle.config.ts

作成されたマイグレーションファイルは以下のようになっている。

0000_superb_magneto.sql
CREATE TABLE IF NOT EXISTS "contact" (
	"id" serial PRIMARY KEY NOT NULL,
	"created_at" timestamp DEFAULT now() NOT NULL,
	"updated_at" timestamp DEFAULT now() NOT NULL,
	"deleted_at" timestamp,
	"phone_number" varchar(20) NOT NULL,
	"email" varchar(256) NOT NULL,
	"user_id" integer
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "user" (
	"id" serial PRIMARY KEY NOT NULL,
	"created_at" timestamp DEFAULT now() NOT NULL,
	"updated_at" timestamp DEFAULT now() NOT NULL,
	"deleted_at" timestamp,
	"first_name" varchar(256) NOT NULL,
	"last_name" varchar(256) NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
 ALTER TABLE "contact" ADD CONSTRAINT "contact_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
 WHEN duplicate_object THEN null;
END $$;

マイグレーションファイルを適用する(push)

pushコマンドを実行する。

npm run drizzle:push
実行結果
> fastify@1.0.0 drizzle:push
> drizzle-kit push:pg --config=./drizzle/drizzle.config.ts

drizzle-kit: v0.20.7
drizzle-orm: v0.29.1

Custom config path was provided, using './drizzle/drizzle.config.ts'
Reading config file 'D:\program\fastify\drizzle\drizzle.config.ts'
[✓] Changes applied

postgreSQLにログインしてテーブルが作成されているかを確認する。

db=# \d user
                                        Table "public.user"
   Column   |            Type             | Collation | Nullable |             Default
------------+-----------------------------+-----------+----------+----------------------------------
 id         | integer                     |           | not null | nextval('user_id_seq'::regclass)
 created_at | timestamp without time zone |           | not null | now()
 updated_at | timestamp without time zone |           | not null | now()
 deleted_at | timestamp without time zone |           |          |
 first_name | character varying(256)      |           | not null |
 last_name  | character varying(256)      |           | not null |
Indexes:
    "user_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "contact" CONSTRAINT "contact_user_id_user_id_fk" FOREIGN KEY (user_id) REFERENCES "user"(id)
db=# \d contact
                                         Table "public.contact"
    Column    |            Type             | Collation | Nullable |               Default
--------------+-----------------------------+-----------+----------+-------------------------------------
 id           | integer                     |           | not null | nextval('contact_id_seq'::regclass)
 created_at   | timestamp without time zone |           | not null | now()
 updated_at   | timestamp without time zone |           | not null | now()
 deleted_at   | timestamp without time zone |           |          |
 phone_number | character varying(20)       |           | not null |
 email        | character varying(256)      |           | not null |
 user_id      | integer                     |           |          |
Indexes:
    "contact_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "contact_user_id_user_id_fk" FOREIGN KEY (user_id) REFERENCES "user"(id)

なお、npm run drizzle:generateを再実行すると、以下のようになり重複してSQLが実行されることはない。

> fastify@1.0.0 drizzle:push
> drizzle-kit push:pg --config=./drizzle/drizzle.config.ts

drizzle-kit: v0.20.7
drizzle-orm: v0.29.1

Custom config path was provided, using './drizzle/drizzle.config.ts'
Reading config file 'D:\program\fastify\drizzle\drizzle.config.ts'
[i] No changes detected

schema.tsを更新してみる

contactテーブルのemailからNOT NULL制約を削除してみる。

export const contact = pgTable('contact', {
  ...schemaBase,
  phoneNumber: varchar('phone_number', { length: 20 }).notNull(),
  email: varchar('email', { length: 256 }),
  userId: integer('user_id').references(() => user.id),
});

再度generateコマンドを実行する。

npm run drizzle:generate
実行結果
> fastify@1.0.0 drizzle:generate
> drizzle-kit generate:pg --config=./drizzle/drizzle.config.ts

drizzle-kit: v0.20.7
drizzle-orm: v0.29.1

Reading config file 'D:\program\fastify\drizzle\drizzle.config.ts'
2 tables
contact 7 columns 0 indexes 1 fks
user 6 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle\0001_productive_big_bertha.sql 🚀
0001_productive_big_bertha.sql
ALTER TABLE "contact" ALTER COLUMN "email" DROP NOT NULL;

再度pushコマンドを実行する。

npm run drizzle:push
実行結果
> fastify@1.0.0 drizzle:push
> drizzle-kit push:pg --config=./drizzle/drizzle.config.ts

drizzle-kit: v0.20.7
drizzle-orm: v0.29.1

Custom config path was provided, using './drizzle/drizzle.config.ts'
Reading config file 'D:\program\fastify\drizzle\drizzle.config.ts'
[✓] Changes applied

再度postgreSQLにログインして制約が削除されているかを確認する。

db=# \d contact
                                         Table "public.contact"
    Column    |            Type             | Collation | Nullable |               Default
--------------+-----------------------------+-----------+----------+-------------------------------------
 id           | integer                     |           | not null | nextval('contact_id_seq'::regclass)
 created_at   | timestamp without time zone |           | not null | now()
 updated_at   | timestamp without time zone |           | not null | now()
 deleted_at   | timestamp without time zone |           |          |
 phone_number | character varying(20)       |           | not null |
 email        | character varying(256)      |           |          |
 user_id      | integer                     |           |          |
Indexes:
    "contact_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "contact_user_id_user_id_fk" FOREIGN KEY (user_id) REFERENCES "user"(id)

DBからマイグレーションファイルとschema.tsを作成してみる(introspect)

introspectコマンドは既にDBにテーブルが作成されていて、マイグレーションファイルが存在しない場合に使用する。

drizzle.config.tsだけを残してファイルを一旦削除して以下のコマンドを実行する。

npm run drizzle:introspect
実行結果
> fastify@1.0.0 drizzle:introspect
> drizzle-kit introspect:pg --config=./drizzle/drizzle.config.ts

drizzle-kit: v0.20.7
drizzle-orm: v0.29.1

Custom config path was provided, using './drizzle/drizzle.config.ts'
Reading config file 'D:\program\fastify\drizzle\drizzle.config.ts'
[✓] 2  tables fetched
[✓] 13 columns fetched
[✓] 0  enums fetched
[✓] 0  indexes fetched
[✓] 1  foreign keys fetched

[✓] Your SQL migration file ➜ drizzle\0000_smiling_skrulls.sql 🚀
[✓] You schema file is ready ➜ drizzle\schema.ts 🚀

schema.tsはdrizzle.config.tsのoutで設定した場所に一緒に出力される。

出力結果
└── drizzle
    ├── meta
    │   ├── _journal.json
    │   └── 0000_snapshot.json
    ├── 0000_smiling_skrulls.sql
    ├── drizzle.config.ts
    └── schema.ts

マイグレーションファイルはコメントアウトされた形で出力される。

0000_smiling_skrulls.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 IF NOT EXISTS "user" (
	"id" serial PRIMARY KEY NOT NULL,
	"created_at" timestamp DEFAULT now() NOT NULL,
	"updated_at" timestamp DEFAULT now() NOT NULL,
	"deleted_at" timestamp,
	"first_name" varchar(256) NOT NULL,
	"last_name" varchar(256) NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "contact" (
	"id" serial PRIMARY KEY NOT NULL,
	"created_at" timestamp DEFAULT now() NOT NULL,
	"updated_at" timestamp DEFAULT now() NOT NULL,
	"deleted_at" timestamp,
	"phone_number" varchar(20) NOT NULL,
	"email" varchar(256),
	"user_id" integer
);
--> statement-breakpoint
DO $$ BEGIN
 ALTER TABLE "contact" ADD CONSTRAINT "contact_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
 WHEN duplicate_object THEN null;
END $$;

*/

Discussion