
SQLとTypeScriptの二重管理にサヨナラ!Drizzle ORMで実現する型安全なエッジDB操作
こんにちは!Accel-Hack(アクセルハック) 開発チームです。
社内AIアシスタント「Airi」の開発プロセスをお届けする開発記ブログ。
第8回目の今回は、Airi のデータアクセスを支える 「Drizzle ORM」 をご紹介します。
前回の Vol.7 では、Cloudflare Workers や D1 を中心としたサーバーレス基盤を構築したお話をしました。
土台が整うと、次に立ちはだかるのが 「データベースの中身を、いかに安全かつ快適に操作するか」 という課題です。
今回は、その悩みを解決してくれた頼れる相棒についてお届けします。
1. SQLの文字列、そろそろ卒業しませんか?
Cloudflare D1 は SQLite ベースのデータベースで、素の状態でも `db.prepare("SELECT ...")` のように SQL 文字列を書けば操作できます。
しかし、このスタイルには見逃せない落とし穴があります。
テーブル名やカラム名を打ち間違えても書いている時点では気づけず、エラーが表面化するのは本番で機能を叩いた瞬間です。
さらに、返ってくる結果は「なんでもあり(`any`)」の状態なので、TypeScript の型チェックや入力補完の恩恵をまったく受けられません。
もっとも地味に効いてくるのが、二重管理の問題です。
カラムを1つ追加するだけで、データベース側のSQLと、アプリ側で扱う型定義の両方を人間が手作業で揃える必要があります。
この同期作業は、AIと二人三脚で機能をどんどん足していく私たちの開発スタイルとは、致命的に相性が悪いものでした。
そこで白羽の矢が立ったのが、Drizzle ORM です。
2. なぜ Drizzle ORM なのか?
データベースとの橋渡し役(ORM)には数多くの選択肢がありますが、私たちが Drizzle を選んだ理由は大きく3つあります。
1つ目は、エッジ環境への公式対応です。
Cloudflare D1 を正式にサポートしているため、Workers 上で余計な工夫なしにそのまま動きます。
2つ目は、圧倒的な軽さ。
余計な依存を抱え込まない設計で、バンドルサイズがシビアなエッジ環境でも足かせになりません。
3つ目は、SQLに近い書き味です。
独自の複雑な作法を新たに覚える必要がなく、これまでのSQLの知識をほぼそのまま活かせ、導入コストが最小限で済みます。
そして何より大きかったのが、スキーマ定義がそのまま TypeScript の型になるという一点です。
テーブルの設計図を書けば、それが型としても、クエリの土台としても、マイグレーションの元ネタとしても機能する。
定義する場所はただ一箇所(Single Source of Truth)。
1章で挙げた二重管理の苦しみから、私たちはようやく解放されました。
3. スキーマ定義がすべての起点になる
Airi では、テーブルの定義を `schema.ts` という1ファイルに集約しています。
たとえば、外部サービスの認証トークンを保存するテーブルはこう書きます。
export const userTokens = sqliteTable(
"user_tokens",
{
id: text("id").primaryKey(),
userId: text("user_id").notNull(),
service: text("service").notNull(),
type: text("type").notNull(),
token: text("token").notNull(),
expiresAt: text("expires_at"),
},
(t) => [unique("user_service_type_idx").on(t.userId, t.service, t.type)],
);見ての通り、素のSQLに近い感覚で読めます。
`notNull()` で必須列を、末尾の `unique(...)` で複合ユニーク制約を宣言するだけ。
そしてこの1つの定義から、TypeScript の型も、型安全なクエリも、データベースへ流し込むマイグレーションSQLも、すべてが自動的に導かれます。
この関係を図にすると、次のようになります。

DBクライアントの準備も拍子抜けするほど簡単です。`drizzle-orm/d1` に、Vol.7 で設定した D1 のバインディングとこのスキーマを渡すだけ。
ホスト名や接続情報の受け渡しは一切不要。
エッジならではの身軽さでデータベースと会話を始められます。
4. 実践:型安全なクエリの気持ちよさ
実際のクエリを見ると、Drizzle の恩恵がよくわかります。
Airi でトークンを検索する処理は、`db.select().from(userTokens).where(...)` という形で、条件を `and(eq(...), eq(...))` のように組み立てます。
ポイントは、条件に `userTokens.service` のようにカラムをオブジェクトとして参照することです。
このおかげで、もしカラム名を打ち間違えれば、それは実行時ではなくコンパイル時のエラーとして即座に赤線で教えてくれます。
1章で悩まされた「本番で初めて気づくミス」が、書いているその場で潰せるわけです。
エディタの補完も効き、列名をいちいち確認する手間もありません。
さらに嬉しいのが、返り値にも自動で型が付くことです。
検索結果に `result[0].token` と書けば、それが文字列だとエディタが理解し、存在しない列を触れば警告が出ます。
登録・更新も同様に型で守られ、「まず検索して、あれば更新・なければ挿入」といった定番処理も安心して組み立てられます。
この安心感が、開発スピードを確実に底上げしてくれました。
5. 泥臭い学び:マイグレーションとローカル開発
正直に告白すると、最初から美しく使えていたわけではありません。
導入当初、マイグレーション(DBの変更)は手書きのSQLで管理しており、スキーマ定義との食い違いが起きやすい、ミスの温床でした。
そこで途中から、`drizzle-kit generate` を使う運用へ切り替えました。
これは、`schema.ts` の変更を検知して、その差分から必要なマイグレーションSQLを自動生成してくれる仕組みです。
人間がSQLを手で書く工程が消え、スキーマとデータベースの不一致という事故はほぼゼロになりました。
本番への反映は Wrangler のコマンドに任せるだけです。
もう一つの工夫が、ローカル開発です。
本番は Cloudflare D1、手元では `bun:sqlite` と、実体は異なりますが、両者に同じマイグレーションSQLを流し込むことでスキーマのズレを吸収しています。
Drizzle が両環境を同じ書き方で包むため、アプリ側のコードは1行も変えずに動作。
「ローカルでは動くのに本番だけ壊れる」という悲劇が起きにくいのも安心材料です。
6. おわりに
Drizzle ORM の導入によって、Airi のデータアクセスは「速く書けて、壊れにくい」状態へと進化しました。
スキーマという一つの真実からすべてが導かれる設計は、AIと共に機能を高速に積み上げる私たちのスタイルと驚くほど綺麗に噛み合っています。
型に守られているからこそ、安心してアクセルを踏み込めるのです。
さて、足回りが固まったところで、次はいよいよ外の世界との対話です。
次回は 「Vol.9: Slack連携の第一歩」 として、Airi がどのように Slack と繋がっていくのかをお届けします。お楽しみに!
この記事は以下の体制で作成されました。
ドラフト作成・執筆: Airi (社内AIアシスタント)
アイデア出し・レビュー: CTO
デザイン: 事務スタッフ
Accel-Hackでは、テクノロジーを駆使して世の中の課題を解決していく仲間を募集しています。
私たちの取り組みに少しでも興味を持っていただけたら、ぜひ採用ページや会社サイトも覗いてみてくださいね。