CLOVER🍀

That was when it all began.

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に任せたくない場合はこういった
方法もありなのかなと思います。

Keycloak 25でアクセスログを有効にする(Quarkusのプロパティを指定する)

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

Keycloakがクライアントなどからどうアクセスされているのかを確認するのに、アクセスログを有効にできたらよさそうと思ってちょっと
調べてみました。

Keycloakでアクセスログを有効にする(Quarkusのプロパティを使う)

まずはKeycloakのログに関するガイドを見てみます。

Configuring logging - Keycloak

が、アクセスログについては特に書かれていません…。

Keycloakの設定にもなさそうです。

All configuration - Keycloak

どうしましょう?ということで、設定のガイドを見てみます。

Configuring Keycloak - Keycloak

できれば使わない方がよいみたいですが、KeycloakのベースになっているQuarkusのプロパティを指定できます。

If possible, avoid using properties directly from Quarkus, because they are unsupported by Keycloak. If your need is essential, consider opening an enhancement request first. This approach helps us improve the configuration of Keycloak to fit your needs.

Configuring Keycloak / Formats for configuration / Format for raw Quarkus properties

使い方としては、Keycloakのインストールディレクトリ内にあるconfディレクトリの中にquarkus.propertiesファイルを作成し、
この中にQuarkusのプロパティを設定します。

指定できるQuarkusのプロパティは、Keycloakが使用しているQuarkus Extensionの範囲に限られます。

https://github.com/keycloak/keycloak/blob/25.0.5/quarkus/runtime/pom.xml#L17-L111

またQuarkusのプロパティにはビルドとランタイムの2種類がありますが、ビルド時のプロパティはKeycloakのbuildコマンドを実行する
必要があります。

Quarkusのアクセスログに関するプロパティはこちら。

HTTP Reference / Configuring HTTP Access Logs

quarkus.http.access-log.enabledプロパティをtrueにすると、アクセスログが有効になります。デフォルトでは標準出力に書き出されます。

フォーマットはデフォルトでCommon Log Formatですが、quarkus.http.access-log.patternプロパティでcombinedlong、そして
書式文字列を使った設定を行うこともできます。

あとはアクセスログをログファイルに出力する設定があったり。

参考)

How to enable acecss logs on keycloak quarkus distribution in json format - Configuring the server - Keycloak

自分としては標準出力にアクセスログが出れば十分なので、その点だけ見ていきましょう。

環境

今回の環境はこちら。

$ bin/kc.sh --version
Keycloak 25.0.5
JVM: 21.0.4 (Eclipse Adoptium OpenJDK 64-Bit Server VM 21.0.4+7-LTS)
OS: Linux 5.15.0-121-generic amd64

Keycloak(Quarkus)のアクセスログを有効にする

Keycloakのインストールディレクトリ内のconfディレクトリは、以下のようになっています。

$ ls -1 conf
cache-ispn.xml
keycloak.conf
README.md
truststores

ここにquarkus.propertiesを追加。

conf/quarkus.properties

quarkus.http.access-log.enabled=true

Keycloakを起動。

$ KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=password bin/kc.sh start-dev

Webコンソールにアクセスしてみます。

$ curl localhost:8080/admin/master/console/

こんな感じでアクセスログが記録されます。

2024-09-14 16:06:41,202 INFO  [io.quarkus.http.access-log] (executor-thread-1) 127.0.0.1 - - [14/Sep/2024:16:06:41 +0000] "GET /admin/master/console/ HTTP/1.1" 200 2895

フォーマットや出力先をファイルにしたい場合などは、Quarkusの設定で変更しましょう。

おわりに

Keycloakのアクセスログを有効にしてみました。

アクセスログに関する設定がないんだ、というのにはちょっと驚きましたが、Quarkusの設定でなんとかなることもあるんだなというのを
知る機会になりました。

というか、Keycloakの設定をしたことがないんですよね。あまりミドルウェアとしてのKeycloakの設定に踏み込むことはない気はして
いますが、こういう機会があったら見ていこうとは思います。