本文書はDrizzle ORMに興味があるのでどのような機能を持っているのか動作確認してみたいという人を対象にDrizzle ORMを利用してデータベースにデータを登録する方法など基本的な機能について動作確認を行っています。本文書を一通り読み進めることでDrizzle ORMがどのようなものか基本的なことは理解できるはずです。

Drizzle ORMはデータベースにMySQL, Postgre, SQLiteなど幅広いデータベースをサポートしていますが本文書ではSQLiteを利用しています。

Drizzle ORMとは

Drizzle ORMはSQL-likeなコードでデータベースを管理/操作することができるTypeScript ORMです。TypeScript ORMといえばPrismaを最初に思い浮かべる人も多いかと思います。DrizzleはPrismaと同様に定義したスキーマから型を生成することができ、型安全にアプリケーションの開発を行うことができます。

If you know SQL — you know Drizzle.とドキュメントに記載されておりSQLの理解ができればDrizzleを比較的簡単に使いこなすことができるようです。

最近ではHeadless CMSの一つであるPayload CMS(https://payloadcms.com/)でもDrizzle ORMを利用することでデータベースにPostgresDBを利用できるようになっています。

ORMはObject Relational Mappingの略で、MySQLやPostgreSQL, SQLiteのようなリレーショナルベースに対してSQLではなくオブジェクトのメソッドを利用して操作を行うことができます。オブジェクトメソッドがどのようなものかは本書を読み進めるうちに理解することができます。

環境の構築

TypeScript環境の設定

TypeScriptを利用できる環境を構築するため動作確認用のディレクトリを作成します。ここではfirst-drizzleという名前にしていますが任意の名前をつけてください。


 % mkdir first-drizzle

作成したfirst-drizzle ディレクトリに移動して”npm init -y”コマンドを実行します。”npm init -y”コマンドを実行するとpackage.jsonファイルが作成されます。


 % cd first-drizzle
 % npm init -y

TypeScriptを利用するために必要となるパッケージのインストールを行います。


 % npm install typescript ts-node @types/node --save-dev

“npx tsc –init”コマンドを実行してTypeScriptの設定ファイルtsconfig.tsファイルを作成します。


 % npx tsc --init

Created a new tsconfig.json with:
                                                                                                    TS
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true


You can learn more at https://aka.ms/tsconfig

TypeScriptを利用するための設定は完了です。

Drizzleのインストール

Drizzleを利用するために必要となるパッケージのインストールを行います。better-sqlite3はSQLiteデータベースを利用するために必要となるパッケージです。接続するデータベースによってインストールするパッケージは異なります。


 % npm install drizzle-orm better-sqlite3

better-sqlite3のTypeのインストールも行います。


 % npm i --save-dev @types/better-sqlite3

drizzle-kitは定義したスキーマファイルを利用してテーブルを作成/更新するために必要なマイグレーションファイルを作成するためのツールです。


 % npm install -D drizzle-kit

ここまでにインストールしたパッケージの確認を行うためpackage.jsonファイルの中身を確認します。


{
  "name": "first-drizzle",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/better-sqlite3": "^7.6.8",
    "@types/node": "^20.10.4",
    "drizzle-kit": "^0.20.6",
    "ts-node": "^10.9.2",
    "typescript": "^5.3.3"
  },
  "dependencies": {
    "better-sqlite3": "^9.2.2",
    "drizzle-orm": "^0.29.1"
  }
}

Drizzleの設定

スキーマファイルやデータベースの接続用のコードを保存するためのプロジェクトディレクトリ直下にdbディレクトリの作成を行います。


 % mkdir db

スキーマファイルの作成

スキーマはデータベースの構造を定義するための情報です。簡単にいうとテーブルがどのような列名で構成され、それらの列にどのようなデータを保存するかを定義することです。

スキーマファイルは1つのファイルにまとめてスキーマを記述する方法とスキーマ毎にファイルを分ける方法がありますが本文書では1つのファイルを利用してスキーマを定義します。dbディレクトリにschema.tsファイルを作成し、todosテーブルをSQLiteデータベースに作成するために以下のスキーマを設定します。todosテーブルはid, name, isCompletedの列で構成され、idはオートインクレメントを設定した列を追加する度に自動で数値が採番されます。nameは文字列が格納されisCompletedには数値が格納されます。


import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';

export const todos = sqliteTable('todos', {
  id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
  name: text('name'),
  isCompleted: integer('isCompleted', { mode: 'boolean' }).notNull().default(false),
});
スキーマの設定方法は接続するデータベースによって異なります。

Migrationファイルの作成

スキーマファイルに記述した内容を元にDrizzle Kitを利用してマイグレーションファイルを作成します。実行する際には—schema オプション schema.ts ファイルを指定します。実行すると drizzle ディレクトリが作成され、mata ディレクトリと sql ファイルが作成されます。ファイル名は自動で命名されます。metaディレクトリの中にはマイグレーションを管理する情報が保存されます。


 % npx drizzle-kit generate:sqlite --schema=./db/schema.ts
drizzle-kit: v0.18.1
drizzle-orm: v0.26.5

1 tables
todos 3 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle/0000_amazing_firestar.sql 🚀
このコマンドを実行しただけでデータベース、テーブルが作成されるわけではありません。

ファイルの拡張子がsqlという名前がつけている通り0000_amazing_firestar.sqlの中にはDDL(Data Definition Language)が記述されておりテーブル作成に利用することができるSQLのcreate文が記述されています。create文なのでそのままSQLiteに接続してテーブルを作成することができます。


CREATE TABLE `todos` (
	`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
	`name` text,
	`isCompleted` integer DEFAULT false NOT NULL
);

DBへの接続

データベースへの接続に利用するためのコードを記述するためdb.tsファイルをdbディレクトリの下に作成します。


import { drizzle, BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
import Database from 'better-sqlite3';

const sqlite = new Database('./db/sqlite.db');
export const db: BetterSQLite3Database = drizzle(sqlite);

migrate(db, { migrationsFolder: './drizzle' });

最後の行にmigrate関数が記述さえていますがこの1行がマイグレーションファイルを元にデータベースの作成やテーブルの作成/更新などを行います。テーブルの構成を変更した場合など必要な時にのみ実行されます。migrationFolderプロパティにマイグレーションファイルが保存されているdrizzleディレクトリを指定します。

SQLiteは先ほど説明した通りファイルベースなのでsqlite.dbという名前でdbディレクトリに保存されるように設定しています。

Drizzleの動作確認

これまでに作成した情報を利用してデータベースのテーブルを操作するコードを記述するためプロジェクトディレクトリ直下にindex.tsファイルを作成します。

index.tsファイルではSQLiteデータベースにアクセスを行うtodosテーブルからデータを取得します。db.select().from(todos)のfromメソッドにはデータを取得していテーブルの情報を指定しています。todosはschema.tsファイルからimportしています。すべてのデータを取得するためallメソッドを設定しています。


import { db } from './db/db';
import { todos } from './db/schema';

function main() {

  const allTodo = db.select().from(todos).all();
  console.log(allTodo);
}

main();

作成したindex.tsファイルを実行します。実行してもtodosテーブルには何もデータが入っていないため空の配列が表示されます。


% npx ts-node index.ts
[]

実行後dbディレクトリを確認するとsqlite.dbファイルが作成されていることがわかります。

allでははく10件文のデータを取得したい場合にはlimitメソッドを利用することができます。


const allTodo = db.select().from(todos).liimit().all();

allメソッドで取得したデータ件数はlengthをつけて確認することができます。


const allTodo = db.select().from(todos).all().length();

getメソッドを利用すると1件のデータを取得することができます。


const singleTodo = db.select().from(todos).get();
//
const singleTodo = db.select().from(todos).limit().get();

データの登録

insertメソッドを利用してテーブルへデータの登録を行います。実行するためにはrunメソッドも必要です。


import { db } from './db/db';
import { todos } from './db/schema';

async function main() {
  const result = db
    .insert(todos)
    .values({ name: 'Learn Drizzle', isCompleted: false })
    .run();
  console.log('result', result);

  const allTodo = db.select().from(todos).all();
  console.log('allTodo', allTodo);
}

main();

index.tsファイルを実行すると先ほどとは異なり、selectメソッドを利用して登録したデータが取得できていることが確認できます。


% npx ts-node index.ts
result { changes: 1, lastInsertRowid: 1 }
allTodo [ { id: 1, name: 'Learn Drizzle', isCompleted: false } ]

Drizzle ORMを利用してSQLiteデータベースへデータが登録できるようになりました。

マイグレーションの動作確認

スキーマファイルの変更を行った場合のデータベースのテーブルに反映させるための方法を確認してみます。

列の追加

schema.tsファイルの実行済みのスキーマにuserId列を追加します。


import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';

export const todos = sqliteTable('todos', {
  id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
  name: text('name'),
  userId: text('useId'),
  isCompleted: integer('isCompleted', { mode: 'boolean' })
    .notNull()
    .default(false),
});

schema.tsファイルを更新後、drizzle-kitを利用してマイグレーションファイルを作成を行います。


 % npx drizzle-kit generate:sqlite --schema=./db/schema.ts
drizzle-kit: v0.18.1
drizzle-orm: v0.26.5

1 tables
todos 4 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle/0001_busy_psynapse.sql 🚀

新たにdrizzleディレクトリに0001_flippant_sway.sqlファイルが作成されます。中身を確認すると追加したdate列に関するalter table文が記述されています。


ALTER TABLE todos ADD `useId` text;

マイグレーションがテーブルに反映されるのか確認するためにselectを実行します。


import { db } from './db/db';
import { todos } from './db/schema';

async function main() {
  const allTodo = db.select().from(todos).all();
  console.log('allTodo', allTodo);
}

main();

取得したデータにuserId列が追加されていることができます。値にはnullが設定されています。


% npx ts-node index.ts                                   
allTodo [ { id: 1, name: 'Learn Drizzle', userId: null, isCompleted: false } ]

列の削除

列の追加を行うことができたので次は追加した列を削除したい場合の動作確認を行います。追加したuserId列を削除するためにスキーマファイルを更新します。


import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';

export const todos = sqliteTable('todos', {
  id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
  name: text('name'),
  isCompleted: integer('isCompleted', { mode: 'boolean' })
    .notNull()
    .default(false),
});

schema.tsファイルを更新後、マイグレーションファイルを作成を行います。


 % npx drizzle-kit generate:sqlite --schema=./db/schema.ts
drizzle-kit: v0.18.1
drizzle-orm: v0.26.5

1 tables
todos 3 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle/0002_volatile_cannonball.sql 🚀

0002_volatile_cannonball.sqlファイルには列の削除を行うAlter TABLE分が記述されています。


ALTER TABLE `todos` DROP COLUMN `useId`;

マイグレーションの内容を反映させるためにindex.tsファイルを実行します。実行すると以下のuserIdが削除されていることが確認できます。


% npx ts-node index.ts                                   
allTodo [ { id: 1, name: 'Learn Drizzle', isCompleted: false } ]

Drop Migration

Drizzle KitにはDrop Migrationを行うコマンドがあります。–outオプションにはマイグレーションディレクトリを指定します。実行するとこれまで作成したマイグレーションの名前が表示されます。選択することでマイグレーションファイルを削除することはできますが削除してもデータベースのテーブルに反映されるわけではありません。


% npx drizzle-kit drop --out ./drizzle                   
drizzle-kit: v0.18.1
drizzle-orm: v0.26.5

Please select migration to drop:
  0000_amazing_firestar     
  0001_busy_psynapse   
❯ 0002_volatile_cannonball

その他

設定ファイル

Drizzle Kitでは設定ファイルを利用することができます。drizzle.config.tsファイルをプロジェクトフォルダ直下に作成してスキーマフォルダやマイグレーションフォルダを指定することができます。


import type { Config } from 'drizzle-kit';

export default {
  schema: './db/schema.ts',
  out: './drizzle',
} satisfies Config;

drizzle.config.tsファイルを作成後はnpx drizzle-kit generate:sqliteを実行する際にオプションに–schemaを設定していましたがschemaの値はdrizzle.config.tsファイルに記述されているため省略することができます。

Typeの設定(InferModel)

index.tsファイルの中でinsertの処理を別の関数insertTodoに分けた場合に引数に型をしない場合のコードを記述します。


import { db } from './db/db';
import { todos } from './db/schema';

const insertTodo = (todo) => {
  return db.insert(todos).values(todo).run();
};

async function main() {
  const result = insertTodo({ name: 'Learn TypeScript', isCompleted: false });
  console.log('result', result);

  const allTodo = db.select().from(todos).all();
  console.log(allTodo);
}

には以下のようにメッセージが表示されます。

型の設定に関するメッセージ
型の設定に関するメッセージ

型を設定するためにInferInsertModelを利用することができます。InferInsertModelを利用して作成した型InsertTodoをinsertTodoの引数のtodoの型として利用することができます。


type insertTodo = InferInsertModel<typeof todos>;

import { db } from './db/db';
import { todos } from './db/schema';
import { InferInsertModel } from 'drizzle-orm';

type insertTodo = InferInsertModel<typeof todos>;

const insertTodo = (todo: InsertTodo) => {
  return db.insert(todos).values(todo).run();
};

async function main() {
  const result = insertTodo({ name: 'Learn TypeScript', isCompleted: 0 });
  console.log('result', result);

  const allTodo = db.select().from(todos).all();
  console.log(allTodo);
}

selectから取得する値に対する型が必要な場合にもInforModeを利用することができます。


import { db } from './db/db';
import { todos } from './db/schema';
import { InferInsertModel, InferSelectModel } from 'drizzle-orm';

type insertTodo = InferInsertModel<typeof todos>;
type Todo = InferSelectModel<typeof todos>;

const insertTodo = (todo: InsertTodo) => {
  return db.insert(todos).values(todo).run();
};

async function main() {
  const result = insertTodo({ name: 'Learn TypeScript', isCompleted: 0 });
  console.log('result', result);

  const allTodo: Todo[] = db.select().from(todos).all();
  console.log(allTodo);
}

main();

それぞれの型情報も確認しておきます。

InsertTodoの型情報
InsertTodoの型情報
Todoの型情報
Todoの型情報

実行したSQLの中身

実行したSQLの中身を確認したい場合にはtoSQLメソッドを利用することができます。


import { db } from './db/db';
import { todos } from './db/schema';

async function main() {
  const query = db.select().from(todos).toSQL();
  console.log(query);
}

main();

index.tsを実行すると実行したSQLが’select “id”, “name”, “isCompleted” from “todos”‘であることがわかります。


 % npx ts-node index.ts                                   
{ sql: 'select "id", "name", "isCompleted" from "todos"', params: [] }

Loggingの設定

実行したSQLの中身を確認するためにloggingの設定を行うことができます。


export const db: BetterSQLite3Database = drizzle(sqlite, { logger: true });

下記のindex.tsファイルを実行します。


import { db } from './db/db';
import { todos } from './db/schema';

async function main() {
  const query = db.select().from(todos).toSQL();
  console.log(query);
}

main();

todosテーブルへのSQL以外にもQueryを確認することができます。migrationに関連するテーブルへのアクセスが行われています。


 % npx ts-node index.ts
Query: 
                        CREATE TABLE IF NOT EXISTS "__drizzle_migrations" (
                                id SERIAL PRIMARY KEY,
                                hash text NOT NULL,
                                created_at numeric
                        )

Query: SELECT id, hash, created_at FROM "__drizzle_migrations" ORDER BY created_at DESC LIMIT 1
Query: BEGIN
Query: COMMIT
{ sql: 'select "id", "name", "isCompleted" from "todos"', params: [] }

SQLデータベースにはtodosテーブルの他にマイグレーションを管理するための__drizzle_migrations, sqlite_sequenceテーブルが作成されます。db.tsファイルからmigrate関数の行を削除するとtodosへのSQL分のみとなります。