なぜ Cloudflare D1 専用の ORM を自作したのか
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 でバンドルした結果です。
| minified | gzipped | |
|---|---|---|
drizzle-orm/d1 + drizzle-orm/sqlite-core | 77.8 kB | 22.2 kB |
d1zzle | 44.1 kB | 15.3 kB |
| −43% | −31% |
念のため書いておくと、これは Drizzle が肥大しているという話ではありません。Drizzle は
sideEffects: false を正しく宣言していて、tree-shaking は極めてよく効きます。
違いは、消えるのが「使われないもの」か「汎用のためのもの」かという一点だけです。
得られたのはサイズだけではない
むしろ本命はこちらでした。対象を 1 つに絞ると、他のデータベースに対応物がない D1 固有の機能を第一級で扱えるようになります。D1 の位置指定リードパス、 バインドパラメータの上限、Sessions API、課金カウンタ。汎用 ORM ではこれらは 「抽象の向こう側」にあり、扱おうとするとエスケープハッチを探すことになります。
自作 OSS を本番で使うということ
d1zzle は公開して終わりではなく、あるくとまるの本番で全面採用し、 認証基盤の Better Auth も同じ ORM の上に乗せています。使いながら見つかった癖は 利用側リポジトリにノートとして書き出し、ライブラリ本体に戻す。ベンダーに issue を 立てて待つ代わりに、自分で直せる。それが、依存を 1 つ自作することの一番の実利でした。
ナミスマート合同会社
事業の課題を、最適なツールを開発して解決します。開発のご相談はお問い合わせからどうぞ。