💨

はじめてのCloudflare D1アプリ

2022/07/31に公開

D1はローカルで動く

5月にClouflare D1が発表されました。ヤバいらしいです。

まだベータすら出てません。PrivateなEarly Accessがありますが、僕はもらってません。でも早く触ってみたい!じゃないっすか。で、よく見てみると、Cloudflare Workersのローカル実行環境のMiniflareにはD1をエミュレートするブランチがもうすでにあるのです。

https://github.com/cloudflare/miniflare/tree/d1-beta-support/packages/d1

また、Cloudfalre Workersの公式CLIであるWranglerはそのMiniflareをローカル向けに内包しています。そしてWranglerにもD1に対応させたブランチがあります。ちょっと前まで動かなかったけど、最近動くようになりました。

https://github.com/cloudflare/wrangler2/pull/1579

ってことは、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の中ではそれを便利なallrunfirstといったメソッドでいじれるようになっているだけです。ローカルモードではその参照先がローカルのSQLiteのファイルに向けています。

では行ってみよう!

準備する

Cloudflareが公式で用意してくているtemplateを参考にしました。

https://github.com/cloudflare/templates/tree/main/worker-d1

まずWranglerのD1ブランチをインストールします。

yarn add -D wrangler@d1

D1コマンドが追加されています。

SS

このコマンドを使うと本番環境に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込みのアプリが速攻で作れました。

SS

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;

全コードはこちらです。

https://github.com/yusukebe/hello-d1

感想

Clouflare WorkersはNode.jsではありません。Web Standardをベースとした環境で、制約のキツいものでした。D1はいうてしまえばSQLiteそのままなのですが、それがCloudflare WorkersをエミュレートしているWrangler/Miniflareで動いているのでなんか感動です。

ほんとはPrismaみたいなORMを動かしたかったのですが、そもそもPrismaなりってコネクタを内包していて、それがいいところなんですが、そうするとD1と繋げないんすよね。ローカルのSQLiteを覗くのはなんか違うし。だから、D1に対応したORMがほしいと思いました!というか作りたいです。

まとめ

以上、「Cloudflare D1を使いたい!」というモチベーションで、いち早くローカルで使って、はじめてのアプリを作ってみました。

Discussion