Hono meets Cloudflare D1(Alpha)

先月あたりにalphaバージョンが公開されたCloudFlare D1をHonoと組み合わせて使います。
Honoは今回初めて使ったのですが、かなりシンプルで使いやすく今後も使っていきたい気分になりました。

:::note info
Hono v2.6.2 + CloudFlare D1(Alpha) で記事を書いています。
:::

:::note warn
CloudFlare D1はAlpha版です。現在も頻繁に更新があるため、記事執筆時点と情報が異なる可能性があります。
:::

# CloudFlare WorkersとHonoをセットアップ

以下のコードで準備完了です。簡単ですね。

```shell
npx wrangler init donesample
cd donesample
npm install hono
```

申し訳程度にHello worldも書いてみます。

```ts:src/index.ts
import { Hono } from "hono";

const app = new Hono();
app.get("/", (c) => c.text("Hello"));

export default app;
```

実行は以下のコマンドで。

```shell
npm start

# curl localhost:8787
# -> Hello
```

# D1を使う

公式のドキュメントを参考に実装します。

https://developers.cloudflare.com/d1/

## データベースを作成する

wranglerコマンドから作成できます。

```shell
npx wrangler d1 create myfirstdone
```

出力は以下です。alphaバージョンなので注意書きがあります。

```shell
🚧 D1 is currently in open alpha and is not recommended for production data and traffic.
Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose.
To request features, visit https://community.cloudflare.com/c/developers/d1.
To give feedback, visit https://discord.gg/cloudflaredev
✅ Successfully created DB 'myfirstdone3'!

Add the following to your wrangler.toml to connect to it from a Worker:

[[ d1_databases ]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "myfirstdone"
database_id = "fddb99d1-b44d-4334-a84e-2a67e57d59ac"
```

`wrangler.toml`を確認するとD1データベースの定義が追加されているはずです。なければさきほどのコンソールの出力をもとに追加してください。

```toml:wrangler.toml
# ...

[[ d1_databases ]]
binding = "DB"
database_name = "myfirstdone"
database_id = "3b9c4f9b-e4d1-4176-8d44-378e97c7aa46"
```

## データベースのセットアップ

`db/schema.sql` を作成します。

```sql:db/schema.sql
DROP TABLE IF EXISTS Urls;
CREATE TABLE Urls (
    ID INT,
    Url TEXT,
    Description TEXT,
    PRIMARY KEY (`Id`)
);
INSERT INTO Urls(
    ID,
    Url,
    Description
) VALUES
    (9999999, 'https://google.com', 'Test url'),
    (9999998, 'https://yahoo.co.jp', 'Test url2')
;

```

以下のコマンドでSQLを実行します。

```shell
wrangler d1 execute myfirstdone --local --file=./db/schema.sql
```

## HonoからD1内のデータを取得する

Honoの公式サンプルをもとにBindingを定義します。

https://github.com/honojs/examples/blob/main/blog/src/bindings.d.ts

### 準備

wranglerコマンドから作成した場合は `D1Database` というクラスが定義されているので、以下のように追記します。

```ts:src/binding.d.ts
export interface Bindings {
  DB: D1Database;
}

declare global {
  function getMiniflareBindings(): Bindings;
}
```

### APIを実装

URLを記録するAPIを例に実装します。

```ts:src/urls.ts
import { Hono } from 'hono';
import { Bindings } from './bindings';

const urls = new Hono<{ Bindings: Bindings }>();

interface Url {
  Id: number;
  Url: string;
  Description: string;
}

urls.get("/", async (ctx) => {
  const env = ctx.env;
  const stmt = env.DB.prepare("SELECT * from Urls");
  const { results } = await stmt.all<Url>();
  return ctx.json({ message: results?.map((r) => r.Url) });
});

export default urls;
```

Honoクラスのインスタンスを作成する際にBindingsを渡さないと、D1の各種メソッドにアクセスする際に型補完が動作しなくなるので注意です。

```ts
const urls = new Hono<{ Bindings: Bindings }>();
```

さきほど定義したBindingsに `DB`経由で `prepare` `all` メソッドで結果にアクセスできます。 

```ts
  const stmt = env.DB.prepare("SELECT * FROM Urls");
  const { results } = await stmt.all<Url>();
```

変数も割り当て可能です。

```ts
env.DB.prepare("SELECT * FROM Urls WHERE Id = ? AND Url = ?").bind(id, url);
```

`all` メソッドでSQLのクエリを実行し結果を取得可能ですが、ほかにもいくつか同類のメソッドがあります。利用ケースにあわせて選択しましょう。以下がメソッド一覧です。

https://developers.cloudflare.com/d1/platform/client-api/#query-statement-methods

- `stmt.first( [column] )`
- `stmt.all()`
- `stmt.raw()`
- `stmt.run()`
- `db.dump()`
- `db.exec()`
- `db.batch()`

### APIルーティングを追加

以下のように追加します。簡単で良いですね。

```ts:src/index.ts
import { Hono } from 'hono';

import urls from './urls';
import { Bindings } from './bindings'

const app = new Hono<{ Bindings: Bindings }>();
app.route("/urls", urls);

export default app;
```

## APIの動作確認

以下のコマンドで開発環境を起動します。

```shell
npx wrangler dev --local --persist
```

起動後、以下のコマンドを実行して開発環境のデータベースをセットアップします。

```shell
wrangler d1 execute myfirstdone --file=./db/schema.sql
```

APIを実行します。

```shell
curl -i http://localhost:8787/urls

# -> {"message":["https://google.com","https://yahoo.co.jp"]}
```

## デプロイ

デプロイはとても簡単です。

```shell
npx wrangler publish
```

DBの初期化などクエリの実行も開発環境とほぼ同じです。

```shell
wrangler d1 execute myfirstdone --file=./db/schema.sql
```

# DBのマイグレーション

なんとマイグレーション機能もwranglerコマンドに実装されていました。

以下のコマンドを実行すると `migrations` ディレクトリが作成されて、 `0000_hoge.sql` というファイルが作成されます。

```shell
wrangler d1 migrations create myfirstdone hoge
```

実行後のSQLファイルの中身は以下です。

```sql
-- Migration number: 0000 	 2022-12-18T08:10:32.404Z
```

SQLファイルを良い感じに書き換えてから以下のコマンドで、マイグレーションを実行します。差分管理もしてくれるので、最後に適用したマイグレーション以降を実行してくれます。実行履歴はDB側に記録している？

```shell
wrangler d1 migrations apply --local myfirstdone
```

# 便利な機能

## 任意のSQLクエリを実行する

以下のコマンドで任意のSQLを実行可能です。 `--local` フラグの有無で実行先も簡単に切り替えられるので気軽に実行できてしまいます。

```shell
wrangler d1 execute myfirstdone --local --command='SELECT * FROM Urls'
```

## DBのバックアップ

以下のコマンドでバックアップも作成可能です。定期的にCloudFlare側で実行されるバックアップもあります。

```shell
wrangler d1 backup create myfirstdone
```

# 最後に

まだalpha版ということですが現状でも全然使いやすい感じで開発体験も良かったです。 `wrangler d1` コマンドも結構整備されていて、標準的なオペレーションは一通りできそうで個人的にはポイント高かったです。

alpha版ということによる制限もありますが、これが正式リリース後どのくらい緩和されるのかが楽しみです。データベースの容量制限なんかは特に気になります。あとはお値段。

https://developers.cloudflare.com/d1/platform/limits/

異なるリージョンからアクセスがあったときのデータ同期も検証したかったのですが、とりあえず記事にしたかったのでまた後日。データベースの外に露出させるインタフェースは読み取り専用、という運用ならだいぶ良い感じに運用できるかもしれません。