Bun × SQLite:ORMを捨て、低レイヤの熱量で「究極の実行効率」を手に入れる
多くのエンジニアが「開発体験(DX)」の名の下に、分厚いORM(Object-Relational Mapping)を積層させ、ランタイムのメモリを浪費させている。しかし、本気でスループットを追求するなら、Bunの組み込みSQLiteモジュールを直接叩くのが最適解だ。
これは単なる「SQL直書き」の推奨ではない。ORMというブラックボックスを排除し、アプリケーションとデータベースの境界を極限まで薄くすることで、コンテキストスイッチと変換オーバーヘッドをゼロに近づけるアーキテクチャの話だ。
—
1. なぜ「ORMなし」がDevOps的に正しいのか
ORMは、クエリの抽象化代償として「予期せぬN+1問題」「型定義の同期ズレ」「メモリ上の巨大なオブジェクト生成」を抱え込む。Bunの `bun:sqlite` は、SQLiteのC APIを直接バインドしているため、V8/JavaScriptCoreのヒープを汚さず、直接バイトコードに近いレベルでデータを取り出せる。
究極の型安全性を実現する「Mapped Types」
ORMの恩恵を捨てても、型の保証は捨ててはならない。TypeScriptの `satisfies` 演算子と組み合わせて、DBスキーマを型として直書きする。
import { Database } from “bun:sqlite”;
// スキーマ定義をTypeScriptのインターフェースとして管理
interface UserRow {
id: number;
email: string;
created_at: string;
}
const db = new Database(“app.db”);
// SQL直書きだが、戻り値の型は明示的に固定する
const getUser = (id: number): UserRow | null => {
return db.query
};
ここで重要なのは、「スキーマの変更=DBマイグレーションと型定義の同時更新」という規律をCIで強制することだ。後述するパイプライン設計でこれを担保する。
—
2. コンテナ環境での「ゼロ・フリクション」自動構成
DockerでBunを運用する場合、SQLiteファイルは永続化ボリュームに置くことになる。ここで最も重要なのは、「コンテナ起動時のWALモード有効化」と「シグナル処理」だ。
Dockerfileでの最適化戦略
`bun:sqlite` は非常に高速だが、デフォルトのジャーナルモードでは書き込み競合が発生しやすい。コンテナ起動時に確実にWAL(Write-Ahead Logging)モードへ切り替える初期化スクリプトを挟む。
軽量なBunイメージを選択
FROM oven/bun:latest
WORKDIR /app
COPY . .
起動時にSQLiteをWALモードへ強制設定するラッパー
これにより、読み込みと書き込みの並行性が劇的に向上する
CMD [“sh”, “-c”, “bun run init-db.ts && bun run src/index.ts”]
`init-db.ts` の中身:
import { Database } from “bun:sqlite”;
const db = new Database(“app.db”);
db.run(“PRAGMA journal_mode = WAL;”); // 高並行書き込みを可能にする必須設定
db.run(“PRAGMA synchronous = NORMAL;”); // ディスクI/Oを最適化しつつ安全性を保つ
—
3. CI/CDパイプライン:型とスキーマの「絶対的一致」
ORMを使わない場合、DBスキーマとコードの乖離は致命的なランタイムエラーを招く。これを防ぐため、CIパイプライン内で「スキーマ・インテグリティ・チェック」を走らせる。
1. Schema Check: `sqlite3` CLIでDBスキーマをダンプし、`.sql` ファイルと整合性が取れているか `diff` を取る。
2. Type Check: 既存のSQLクエリからTypeScript型を自動生成する(`drizzle-kit`のようなツールを、コード生成のためだけに使うのが賢い)。
.github/workflows/ci.yml の抜粋
jobs:
validate-db:
runs-on: ubuntu-latest
steps:
- uses: oven-sh/setup-bun@v1
- run: |
# 開発環境のスキーマとCI環境のDBを比較
sqlite3 test.db < schema.sql
# 不整合があれば即座にCIを落とす
[ -z "$(git diff schema.sql)" ] || exit 1
---
4. メモリとアーキテクチャのハック:コネクションプールを超えて
BunのSQLiteは、Node.jsの `better-sqlite3` とは異なり、Bunのイベントループに深く統合されている。
- Statementのキャッシュ: `db.prepare()` は非常に高コストだ。SQLの文字列を頻繁に生成せず、一度生成した `Statement` オブジェクトをMapで保持し、再利用せよ。
- 共有メモリの活用: 大規模な読み取りが発生する場合、SQLiteファイルを `/dev/shm`(メモリファイルシステム)上に配置し、起動時にコンテナボリュームから同期する手法が、最高速の応答を叩き出す。
実践:ステートメント再利用ハック
const cache = new Map();
function getQuery(sql: string) {
if (!cache.has(sql)) {
cache.set(sql, db.prepare(sql));
}
return cache.get(sql);
}
// 呼び出し側
const user = getQuery(“SELECT FROM users WHERE id = ?”).get(userId);
—
結論:プロの矜持として
ORMは「楽をするための道具」だが、アーキテクトは「制御するための道具」を選ぶ。
BunとSQLiteを組み合わせることで、Node.js時代には考えられなかった低レイテンシなバックエンドが構築できる。抽象化の層を一枚剥ぎ取り、OSとファイルシステム、そしてCPUの特性を理解した上でコードを書く。その先にこそ、真の「爆速」と「堅牢さ」が共存する開発環境がある。
あなたが今、ORMの魔法で隠蔽されている問題に苦しんでいるなら、まずは今日、一行のクエリを `db.prepare()` することから始めてほしい。そこには、純粋な計算資源の息吹を感じられるはずだ。