【第30回】診断ツールと同じ仕組みを自分で作る|D1(嫁子が教えるTS講座)
今回のゴール
- D1にテーブルを作って、読み書きできる
- 設定に書いたバインディングが、そのまま型になる仕組みが分かる
- 第26回のPrismaとの違いを説明できる
前回(Cloudflare Workers)で作った関数に、データの保存を足します。全体の流れは講座の目次へ。
嫁子: D1はCloudflareのデータベースで、中身は第25回でやったSQLiteなの。
だから前に覚えたSQLがそのまま使えるよ。新しく覚えることは、実はそんなに多くないからね。
1. つなぎ方は「設定に書く」
Workersでは、データベースの接続情報をコードに書きません。設定ファイルに「これを使う」と書くと、env から触れるようになります。
wrangler.jsonc に足します。
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2026-09-11",
"d1_databases": [
{
"binding": "DB",
"database_name": "my-app-db",
"database_id": "local-placeholder"
}
]
}
binding に書いた名前が、そのままコードでの呼び名になります。ここでは DB なので env.DB です。
この方式をバインディングと呼びます。第23回で .env.local に接続情報を書いたのとは考え方が違って、接続文字列を持ち歩かずに済みます。
嫁子: 鍵を書かなくていいのは、地味だけど大きいよ。
第23回でさんざん「漏らすな」って言ったでしょ。そもそも書く場所が無ければ、漏らしようがないからね。
2. 設定が、そのまま型になる
ここがTypeScriptの講座として面白いところです。次を実行します。
npm run cf-typegen
すると型定義ファイルが作られ、中にこう書かれます。
interface __BaseEnv_Env {
DB: D1Database;
}
設定ファイルに DB と書いたから、env.DB が D1Database 型になったということです。手で型を書いていません。
第26回のPrismaでは「スキーマから型が作られる」と書きました。ここでは設定から型が作られます。仕組みは違いますが、1か所書けば型がついてくるという発想は同じです。
設定を変えたら、もう一度実行してください。バインディングを足したのに env に出てこないときは、たいていこれを忘れています。
3. テーブルを作る
SQLをファイルに書きます。schema.sql を作ってください。
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
第25回で読み方を覚えた CREATE TABLE です。今度は自分で書きます。
流し込むコマンドはこれです。
npx wrangler d1 execute my-app-db --local --file=./schema.sql
--local が付いているのが重要です。これを付けている間は手元のファイルに対して実行され、Cloudflareのアカウントは要りません。前回と同じで、試すだけなら登録なしで進みます。
嫁子: 本番のデータベースを作るときは
--localを外すの。外した瞬間に本物へ行くから、そこだけ気をつけてね。第25回で言った
WHEREの付け忘れと同じで、1語の差で対象が変わるやつだよ。
4. 読み書きする
src/index.ts から使います。
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/items" && request.method === "GET") {
const { results } = await env.DB.prepare(
"SELECT id, name, done FROM items ORDER BY id DESC"
).all();
return Response.json(results);
}
if (url.pathname === "/api/items" && request.method === "POST") {
const body = await request.json<{ name?: unknown }>();
const name = String(body.name ?? "").trim();
if (name === "") {
return Response.json({ error: "名前が空です" }, { status: 400 });
}
const item = await env.DB.prepare(
"INSERT INTO items (name) VALUES (?) RETURNING id, name, done"
)
.bind(name)
.first();
return Response.json(item, { status: 201 });
}
return new Response("Not Found", { status: 404 });
},
} satisfies ExportedHandler<Env>;
読み方は3つだけです。
| 書き方 | 意味 |
|---|---|
.prepare(SQL) |
SQLを用意する |
.bind(値) |
? の場所に値を入れる |
.all() / .first() / .run() |
全部取る/1件取る/実行だけ |
入力チェックは第24回と同じようにサーバー側で書いています。置き場所がWorkersに変わっても、第23回の原則は変わりません。
? を必ず使う
いちばん大事なのはここです。値を文字列として直接つなげないでください。
// ✕ 絶対にやらない
`INSERT INTO items (name) VALUES ('${name}')`
// ○ ? と bind を使う
"INSERT INTO items (name) VALUES (?)"
上のように書くと、入力した文字がSQLの一部として解釈されてしまいます。名前欄に細工した文字を入れるだけで、テーブルを消すこともできてしまう、という有名な攻撃につながります。
嫁子: AIが書いたSQLを見るときの検査項目、もう1つ追加ね。
第25回で「
UPDATEとDELETEにWHEREがあるか」って言ったでしょ。それに「値が?になっているか」を足して2つ。この2つだけ見れば、大きな事故はだいたい防げるよ。
5. 動かして確かめる
起動します。
npm run dev
第24回でやったように、窓口だけ先に確認します。実際に試すと、こう返ってきました。
POST {"name":"牛乳"} → 201 {"id":1,"name":"牛乳","done":0}
POST {"name":"卵"} → 201 {"id":2,"name":"卵","done":0}
POST {"name":" "} → 400 {"error":"名前が空です"}
GET /api/items → 200 [{"id":2,...},{"id":1,...}]
wrangler dev を止めて起動し直しても、データは残っています。手元のファイルに保存されているためです。
done が 0 なのはなぜか
気づいた方もいると思います。done が false ではなく 0 で返っています。
SQLiteには真偽値の型がなく、0 と 1 で表すためです。第25回で型の対応表を出しましたが、実際に触ると、ここで差が出ます。
そのまま画面に渡すと、第6回で見た落とし穴を踏みます。0 は条件判定で false 扱いなので、たまたま動いてしまうこともありますが、done === false のような比較は成立しません。
受け取った側で変換しておくのが安全です。
const items = results.map((row) => ({
...row,
done: row.done === 1,
}));
嫁子: 第26回のPrismaは、これを勝手にやってくれてたの。
falseで返ってきたでしょ。D1を直接触るなら、境界で自分で直す。どっちが良いという話じゃなくて、やってくれる道具を使うか、自分でやるかの違いだね。
6. Prismaとの違い
第26回と並べると、選び方がはっきりします。
| Prisma(第26回) | D1を直接 | |
|---|---|---|
| SQL | ほぼ書かない | 自分で書く |
| 型 | スキーマから生成 | 設定から生成(バインディングのみ) |
| 取得した行の型 | 列ごとに付く | 自分で指定する |
| 真偽値・日付 | 変換してくれる | 自分で変換する |
| 導入 | パッケージと設定が要る | Workersに最初から入っている |
小さいものほどD1を直接触るほうが速く、大きくなるほどPrismaの型が効いてきます。
このブログに診断ツールを1つ足す、くらいの規模なら直接で十分です。逆に画面が10枚あるアプリなら、第26回で書いた「列を足すと型も更新される」の価値が大きくなります。
つまずきポイント
database_id は、本番用を作ると実際の値が入ります。手元で試すだけなら仮の文字列で動きますが、公開するときは npx wrangler d1 create で作った値に置き換えます(ここでアカウントが必要です)。
--local と本番は別のデータベースです。手元で入れたデータは本番には行きません。逆も同じです。
取得した行の型は自動では付きません。.all() の結果は素のままなので、使う側で形を指定するか、上のように変換してから渡してください。
今回のまとめ
- 接続は設定ファイルのバインディングで行う。接続文字列を書かずに済む
npm run cf-typegenで、設定がそのまま型になる(DB: D1Database)- SQLは
.prepare()→.bind()→.all()/.first()/.run() - 値は必ず
?とbind。文字列で組み立てない --localの間はアカウント不要。外すと本番に行く- 真偽値は
0/1で返る。境界で変換する - Prismaとの違いは「やってくれる道具を使うか、自分でやるか」
次にやること
次回が最終回で、ここまでで学んだものを実際にこのブログへ組み込みます。静的なサイトはそのままに、小さなツールを1つ足す、という形です。
今回の宿題:done を 1 に更新する窓口(PUT か PATCH)を足してみてください。WHERE id = ? を忘れないことが、そのまま今日の復習になります。
嫁子: 第6部に入ってから、新しく覚えることが減ってると思わない?
前に覚えたものが、置き場所を変えて出てきてるだけだからね。それが分かるようになったのが、この講座で得たものだと思うよ。