なぜ Cloudflare D1 専用の ORM を自作したのか

CloudflareD1TypeScriptOSS

Cloudflare Workers 上のプロダクトを書いていて、バンドルサイズの内訳を眺めたときに 気づいたことがあります。ORM の大部分は、私が使っていないデータベースのために 存在している

tree-shaking は「汎用性」を削れない

よくある誤解として、「未使用のコードはバンドラが落としてくれる」というものがあります。 これは半分正しく、半分間違いです。tree-shaking が落とせるのは 到達不能なコード で あって、汎用的なコード ではありません。

汎用 ORM の SQLite エントリポイントから到達可能なものを並べると、こうなります。

  • 方言(dialect)の間接層 — どの DB 向けに SQL を組み立てるかの分岐
  • トランザクション / セーブポイントのサブシステム
  • 同期ドライバと非同期ドライバの両方を覆う prepared statement の抽象

これらは全部、あなたのクエリ 1 本から到達可能です。だからバンドラは落とせません。 落とすには、ソースの時点で存在しない 必要があります。

対象を 1 つに絞る

そこで、Cloudflare D1 と Workers だけ を対象にした ORM を書きました。 d1zzle です。API は Drizzle からそのまま受け継いでいるので、 移行コストはほぼゼロです。

import { drizzle, eq, integer, sqliteTable, text } from 'd1zzle';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  email: text('email').notNull().unique(),
});

const db = drizzle(env.DB);
await db.select().from(users).where(eq(users.id, 1)).get();

ドライバ・スキーマ DSL・クエリ 1 本を含む Worker を esbuild でバンドルした結果です。

minifiedgzipped
drizzle-orm/d1 + drizzle-orm/sqlite-core77.8 kB22.2 kB
d1zzle44.1 kB15.3 kB
−43%−31%

念のため書いておくと、これは Drizzle が肥大しているという話ではありません。Drizzle は sideEffects: false を正しく宣言していて、tree-shaking は極めてよく効きます。 違いは、消えるのが「使われないもの」か「汎用のためのもの」かという一点だけです。

得られたのはサイズだけではない

むしろ本命はこちらでした。対象を 1 つに絞ると、他のデータベースに対応物がない D1 固有の機能を第一級で扱えるようになります。D1 の位置指定リードパス、 バインドパラメータの上限、Sessions API、課金カウンタ。汎用 ORM ではこれらは 「抽象の向こう側」にあり、扱おうとするとエスケープハッチを探すことになります。

自作 OSS を本番で使うということ

d1zzle は公開して終わりではなく、あるくとまるの本番で全面採用し、 認証基盤の Better Auth も同じ ORM の上に乗せています。使いながら見つかった癖は 利用側リポジトリにノートとして書き出し、ライブラリ本体に戻す。ベンダーに issue を 立てて待つ代わりに、自分で直せる。それが、依存を 1 つ自作することの一番の実利でした。


ナミスマート合同会社

事業の課題を、最適なツールを開発して解決します。開発のご相談はお問い合わせからどうぞ。