Closed40

Drizzle で遊ぶ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

このスクラップについて

以前から気になっていて触れていなかった Drizzle ORM を今更ながら触ってみよう。

https://orm.drizzle.team/

Cloudflare Workers から D1 へアクセスするところまでは調べたい。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

パッケージのインストール

コマンド
cd ~/workspace
mkdir scrap-drizzle
pnpm i drizzle-orm @libsql/client dotenv
pnpm i -D drizzle-kit tsx
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

index.ts の作成

コマンド
mkdir src
touch src/index.ts
src/index.ts
import "dotenv/config";
import { drizzle } from "drizzle-orm/libsql";

const db = drizzle(process.env.DB_FILE_NAME!);
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

schema.ts の作成

コマンド
mkdir src/db
touch src/db/schema.ts
src/db/schema.ts
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const usersTable = sqliteTable("users_table", {
  id: int().primaryKey({ autoIncrement: true }),
  name: text().notNull(),
  age: int().notNull(),
  email: text().notNull().unique(),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Drizzle config の作成

コマンド
touch drizzle.config.ts
drizzle.config.ts
import "dotenv/config";
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  out: "./drizzle",
  schema: "./src/db/schema.ts",
  dialect: "sqlite",
  dbCredentials: {
    url: process.env.DB_FILE_NAME!,
  },
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

DB 変更の適用

コマンド
pnpm drizzle-kit push
コンソール出力
No config path provided, using default 'drizzle.config.ts'
Reading config file '/Users/susukida/workspace/scrap-drizzle/drizzle.config.ts'
[✓] Pulling schema from database...
[✓] Changes applied
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

レコードの挿入と選択

src/index.ts
import "dotenv/config";
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/libsql";
import { usersTable } from "./db/schema";

const db = drizzle(process.env.DB_FILE_NAME!);

async function main() {
  const user: typeof usersTable.$inferInsert = {
    name: "John",
    age: 30,
    email: "john@example.com",
  };

  await db.insert(usersTable).values(user);
  console.log("New user created!");

  const users = await db.select().from(usersTable);
  console.log("Getting all users from the database: ", users);

  await db
    .update(usersTable)
    .set({ age: 31 })
    .where(eq(usersTable.email, user.email));

  console.log("User info updated!");

  await db.delete(usersTable).where(eq(usersTable.email, user.email));
  console.log("User deleted!");
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実行

コマンド
pnpm tsx src/index.ts
コンソール出力
New user created!
Getting all users from the database:  [ { id: 1, name: 'John', age: 30, email: 'john@example.com' } ]
User info updated!
User deleted!
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Cloudflare Workers で使う

コマンド
cd workspace
pnpm create hono@latest -i -p pnpm -t cloudflare-workers scrap-drizzle-d1
cd scrap-drizzle-d1
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

D1 データベースの作成

https://developers.cloudflare.com/d1/get-started/#2-create-a-database

コマンド
pnpm wrangler d1 create scrap-drizzle-d1
コンソール出力(例)
 ⛅️ wrangler 4.36.0
───────────────────
✅ Successfully created DB 'scrap-drizzle-d1' in region APAC
Created your new D1 database.

To access your new D1 Database in your Worker, add the following snippet to your configuration file:
{
  "d1_databases": [
    {
      "binding": "scrap_drizzle_d1",
      "database_name": "scrap-drizzle-d1",
      "database_id": "xxxx"
    }
  ]
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

wrangler.json の編集

wrangler.json(例)
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "scrap-drizzle-d1",
  "main": "src/index.ts",
  "compatibility_date": "2025-09-13",
  "compatibility_flags": ["nodejs_compat"],
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "scrap-drizzle-d1",
      "database_id": "xxxx"
    }
  ]
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

テーブルの作成

コマンド
mkdir src/db
touch src/db/schema.ts
src/db/schema.ts
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const usersTable = sqliteTable("users", {
  id: int().primaryKey({ autoIncrement: true }),
  name: text().notNull(),
  age: int().notNull(),
  email: text().notNull().unique(),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Drizzle config の作成

コマンド
touch drizzle.config.ts
drizzle.config.ts
import "dotenv/config";
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  out: "./drizzle",
  schema: "./src/db/schema.ts",
  dialect: "sqlite",
  driver: "d1-http",
  dbCredentials: {
    accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
    databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
    token: process.env.CLOUDFLARE_D1_TOKEN!,
  },
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

.env の作成

コマンド
touch .env
.env(例)
CLOUDFLARE_ACCOUNT_ID="xxxx"
CLOUDFLARE_DATABASE_ID="yyyy"
CLOUDFLARE_D1_TOKEN="zzzz"

各値の取得方法についてはこちら。

https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit

To get accountId go to Workers & Pages -> Overview -> copy Account ID from the right sidebar.
To get databaseId open D1 database you want to connect to and copy Database ID.
To get token go to My profile -> API Tokens and create token with D1 edit permissions.

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

データベースの作成

コマンド
pnpm drizzle-kit push
コンソール出力
No config path provided, using default 'drizzle.config.ts'
Reading config file '/Users/susukida/workspace/scrap-drizzle-d1/drizzle.config.ts'
[✓] Pulling schema from database...
[✓] Changes applied
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

src/index.ts
import { drizzle } from "drizzle-orm/d1";
import { Hono } from "hono";
import { usersTable } from "./db/schema";

const app = new Hono<{
  Bindings: CloudflareBindings;
}>();

app.get("/", async (c) => {
  const db = drizzle(c.env.DB);
  const users = await db.select().from(usersTable).all();

  return c.json(users);
});

export default app;
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

エラー発生

Error: D1_ERROR: no such table: users: SQLITE_ERROR

おそらくローカルの方にはまだテーブルがないことが原因だろう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

マイグレーションの生成

コマンド
pnpm drizzle-kit generate
drizzle/0000_absurd_emma_frost.sql
CREATE TABLE `users` (
	`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
	`name` text NOT NULL,
	`age` integer NOT NULL,
	`email` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

マイグレーションの実行

wrangler.json(追記)
{
  "d1_databases": [
    {
      "migrations_dir": "./drizzle"
    }
  ]
}
コマンド
pnpm wrangler d1 migrations apply scrap-drizzle-d1 --local
コンソール出力(例)
 ⛅️ wrangler 4.36.0 (update available 4.37.0)
─────────────────────────────────────────────
Migrations to be applied:
┌────────────────────────────┐
│ name                       │
├────────────────────────────┤
│ 0000_absurd_emma_frost.sql │
└────────────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on local database scrap-drizzle-d1 (xxxx) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 3 commands executed successfully.
┌────────────────────────────┬────────┐
│ name                       │ status │
├────────────────────────────┼────────┤
│ 0000_absurd_emma_frost.sql │ ✅     │
└────────────────────────────┴────────┘
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

POST 追加

src/index.ts
import { drizzle } from "drizzle-orm/d1";
import { Hono } from "hono";
import { usersTable } from "./db/schema";

const app = new Hono<{
  Bindings: CloudflareBindings;
}>();

app.get("/users", async (c) => {
  const db = drizzle(c.env.DB);
  const users = await db.select().from(usersTable);

  return c.json(users);
});

app.post("/users", async (c) => {
  const db = drizzle(c.env.DB);
  await db.insert(usersTable).values({
    name: "John Doe",
    age: 30,
    email: "john.doe@example.com",
  });

  return c.status(201);
});

export default app;
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

動作確認

コマンド
curl -X POST http://localhost:8787/users
curl http://localhost:8787/users | jq
コンソール出力(例)
[
  {
    "id": 1,
    "name": "John Doe",
    "age": 30,
    "email": "john.doe.1757922881376@example.com"
  }
]
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

デプロイ

コマンド
pnpm deploy

Worker has workers.dev disabled, but 'workers_dev' is not in the config

警告メッセージが表示された。

wrangler.json に "workers_dev": true を追記すれば消えそうだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

リモートでの動作確認

コマンド
curl https://scrap-drizzle-d1.tatsuyasusukida.workers.dev/users | jq
コンソール出力
[]
コマンド
curl -X POST https://scrap-drizzle-d1.tatsuyasusukida.workers.dev/users
コマンド
curl https://scrap-drizzle-d1.tatsuyasusukida.workers.dev/users | jq
コンソール出力(例)
[
  {
    "id": 1,
    "name": "John Doe",
    "age": 30,
    "email": "john.doe.1757923262413@example.com"
  }
]
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

リモートのマイグレーション

気になったのでやってみる。

コマンド
pnpm wrangler d1 migrations apply scrap-drizzle-d1
コンソール出力
 ⛅️ wrangler 4.36.0 (update available 4.37.0)
─────────────────────────────────────────────
✅ No migrations to apply!

こちらは drizzle-kit push を使ってやったのでうまくいかないかと思ったが大丈夫だった。

ただ怖いので wrangler の migrations 機能の方を使うことにしよう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

おわりに

ほんのさわりだったが無事に Cloudflare D1 を Drizzle で使用できることがわかって良かった。

D1 は有料プランであれば 10 GB のデータベースサイズまで利用できるので、ちょっとした個人的な Web アプリを作る程度であれば十分に利用できそうだ。

https://developers.cloudflare.com/d1/platform/limits/

そろそろ Workers Paid プランを契約するときなのかも知れない。

https://www.cloudflare.com/plans/developer-platform/

このスクラップは1日前にクローズされました