はじめてのCloudflare D1アプリ
D1はローカルで動く
5月にClouflare D1が発表されました。ヤバいらしいです。
まだベータすら出てません。PrivateなEarly Accessがありますが、僕はもらってません。でも早く触ってみたい!じゃないっすか。で、よく見てみると、Cloudflare Workersのローカル実行環境のMiniflareにはD1をエミュレートするブランチがもうすでにあるのです。
また、Cloudfalre Workersの公式CLIであるWranglerはそのMiniflareをローカル向けに内包しています。そしてWranglerにもD1に対応させたブランチがあります。ちょっと前まで動かなかったけど、最近動くようになりました。
ってことは、D1はベータすら出てないけど、ローカルでは動くということで、はじめてのD1アプリを作ってみました。
Internal
作る前にMiniflareとWranglerのソースを見るんですが、これ単なるSQLiteですね。 「D1のバックエンドはSQLiteである」というのは公式もアナウンスしている通りなんですが、Miniflareではbetter-sqlite3というドライバを使っています。Wranglerの本番モードではAPIを叩いているのですが、ローカルモードでは後述する、とあるディレクトリに置いたxxx.sqlite3
といったSQLite3のバイナリを読んでいます。つまりSQLiteまんまです。なので、これから紹介するアプリは「一般的なSQLiteを使ったアプリケーション」と同じようなアプリになるでしょう。
Wranglerでは本番モードも実装されていて、それはAPIを叩いてます。
// https://github.com/cloudflare/wrangler2/blob/63cc16937a49dbe3f0ae48d9e67739ea007a10ab/packages/wrangler/templates/d1-beta-facade.js#L59
async _send(endpoint, query, params) {
const body = JSON.stringify(
typeof query == "object"
? query.map((s, index) => {
return { sql: s, params: params[index] };
})
: {
sql: query,
params,
}
);
const response = await this.binding.fetch(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
},
body,
});
//...
}
APIのエンドポイントに対して、プリペアドステートメントのSQL文とBindをJSONにした文字列を送っています。Wranglerの中ではそれを便利なall
、run
、first
といったメソッドでいじれるようになっているだけです。ローカルモードではその参照先がローカルのSQLiteのファイルに向けています。
では行ってみよう!
準備する
Cloudflareが公式で用意してくているtemplateを参考にしました。
まずWranglerのD1ブランチをインストールします。
yarn add -D wrangler@d1
D1コマンドが追加されています。
このコマンドを使うと本番環境にDBを作ったり、リストアップしたり、削除したりできます。今は使いません。
次に、SQL文を用意しましょう。雑な「Blog」アプリにするのでこれにしましょう。
CREATE TABLE post (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL
);
そして、SQLiteのファイルを./wrangler-local-state/d1/DB.sqlite3
というパスに作ります。最近のWranglerではexperimentalでこの./wrangler-local-state/
というディレクトリを見ます。
sqlite3 wrangler-local-state/d1/DB.sqlite3 < ./blog.sql
最後にwrangler.toml
にD1の設定を書き込みます。
[[ d1_databases ]]
binding = "DB"
database_name = "db"
database_id = ""
binding
の項目が重要で、これがアプリ内で使うバインディングの変数名になるし、SQLiteのファイル名を指定することになります。
これで、以下のオプションをつければD1に対応できます。
wrangler dev src/index.tsx --local --experimental-enable-local-persistence
アプリを書く
アプリを書きましょう。Honoを使います。
先程指定した「DB」という名前でバインディングを作り、HonoにGenericsで渡します。こうすることでc.env.DB
で補完が効くようになります。
import { Hono } from 'hono'
import type { Database } from '@cloudflare/d1'
interface Env {
DB: Database
}
const app = new Hono<Env>()
DBはenv
から渡ってくるので、ハンドラの中ではこのようにアクセスできます。
app.get("/", async (c) => {
const response = await c.env.DB.prepare(
`SELECT id,title,body FROM post`
).all();
//...
});
簡単です!HonoはJSXのミドルウェアも備えているので、それと組み合わせるとUI込みのアプリが速攻で作れました。
src/index.tsx
はこのようになりました。
import { Hono } from "hono";
import { jsx } from "hono/jsx";
import type { Database } from "@cloudflare/d1";
import { Top } from "./Top";
import type { Post } from "./Top";
interface Env {
DB: Database;
}
const app = new Hono<Env>();
app.get("/", async (c) => {
const response = await c.env.DB.prepare(
`SELECT id,title,body FROM post`
).all();
const posts: Array<Post> = response.results;
return c.html(<Top posts={posts} />);
});
app.post("/post", async (c) => {
const { title, body } = await c.req.parseBody();
if (title && body) {
await c.env.DB.prepare(`INSERT INTO post(title, body) VALUES(?, ?);`)
.bind(title, body)
.run();
}
return c.redirect("/");
});
export default app;
全コードはこちらです。
感想
Clouflare WorkersはNode.jsではありません。Web Standardをベースとした環境で、制約のキツいものでした。D1はいうてしまえばSQLiteそのままなのですが、それがCloudflare WorkersをエミュレートしているWrangler/Miniflareで動いているのでなんか感動です。
ほんとはPrismaみたいなORMを動かしたかったのですが、そもそもPrismaなりってコネクタを内包していて、それがいいところなんですが、そうするとD1と繋げないんすよね。ローカルのSQLiteを覗くのはなんか違うし。だから、D1に対応したORMがほしいと思いました!というか作りたいです。
まとめ
以上、「Cloudflare D1を使いたい!」というモチベーションで、いち早くローカルで使って、はじめてのアプリを作ってみました。
Discussion