Cloudflare D1 のチュートリアルをやってみる

[2022年に発表され](https://blog.cloudflare.com/ja-jp/introducing-d1-ja-jp/)話題となっている [Cloudflare D1](https://developers.cloudflare.com/d1/)、未だアルファ版のようですがすでにいろいろと触ってみた方も多いかと思います。
自分はVantiqという会社で自社製品であるVantiq Platform 上のアプリケーション開発・アーキテクチャ設計を業務として行っているわけですが、様々なクラウドサービスと自社製品をどう組み合わせたら面白いことができるかなと日々考えたりもしています。
Cloudflare D1 も気になっていたところで、[公式のチュートリアル](https://developers.cloudflare.com/d1/tutorials/build-a-comments-api/) をやってみて、実際に手を動かして概要を理解してみたいと思います。

## Cloudflare D1 とは
Cloudflareが提供する、エッジロケーションに配置されたSQLiteベースのリレーショナルデータベースです。
概要は公式ドキュメントや他に秀逸な記事があるので読んでいただくとして、個人的に興味深いのは

- 分散データベースでありながら
- トランザクションの一貫性を確保している
https://blog.cloudflare.com/whats-new-with-d1/
https://www.infoq.com/news/2022/10/cloudflare-d1-database-sqlite/

という点です。これらを実現した上で[コスト面の優位性](https://zenn.dev/mizchi/articles/cloudflare-d1)もあるのであれば確かにゲームチェンジャーと言えるかも…？



## チュートリアルをやってみる
https://developers.cloudflare.com/d1/tutorials/build-a-comments-api/ 
ではさっそくチュートリアルをやってみましょう。Node環境の構築は事前に済んでいるものとします。

### [Set up your project](https://developers.cloudflare.com/d1/tutorials/build-a-comments-api/#set-up-your-project)
Cloudflare WorkersはサーバーレスなJavaScriptコードの実行環境です。D1はWorkersと統合されているため、まずはWorkersプロジェクトを作成します。
wranglerはCloudflareの開発者用CLIツールで、Workersの開発やデプロイに使用します。

```bash
$ mkdir d1-example && cd d1-example
$ npm init -y && npm install -D wrangler
$ npx wrangler init
```

チュートリアルでは、[Hono](https://yusukebe.com/posts/2022/hono-40-things/)というCloudflare Workers用のWebフレームワークを使うことになっています。ここで入れておきましょう。
```bash
$ npm install hono
```

`npx wrangler init` でプロジェクトスケルトンが生成されています。`src/index.js` にAPIのエンドポイントを定義していきます。
チュートリアルではブログにコメントを投稿する機能を実現するためのテーブル・APIを作成することになっていますがせっかくなので違うことをやってみましょう。

Twitter存続の危機が話題になっているので、D1とWorkersでTwitterクローンを作ってみます。
テーブル構造とAPIは[ChatGPT](https://chat.openai.com/chat)に考えてもらいましょう。

#### Twitterのような機能をもつWebサービスを作成したいです。API仕様を考えてください。

:::note
Twitterのような機能を持つWebサービスを作成するために必要なAPI仕様には、以下のようなものがあります。

ユーザー登録・認証API: ユーザーがアカウントを作成し、ログインするために使用するAPI。

ツイート投稿API: ユーザーが新しいツイートを投稿するために使用するAPI。

ツイート取得API: ユーザーが自分のタイムラインや他のユーザーのツイートを取得するために使用するAPI。

ツイート削除API: ユーザーが自分のツイートを削除するために使用するAPI。

フォロー・アンフォローAPI: ユーザーが他のユーザーをフォローしたり、アンフォローするために使用するAPI。

リツイートAPI: ユーザーが他のユーザーのツイートをリツイートするために使用するAPI。

いいねAPI: ユーザーが他のユーザーのツイートにいいねを付けるために使用するAPI。

検索API: ユーザーが特定のキーワードでツイートを検索するために使用するAPI。
:::

はい、たくさんありますね。ここでは、最低限の機能を実現するため

- ツイート投稿API: ユーザーが新しいツイートを投稿するために使用するAPI。
- ツイート取得API: ユーザーが自分のタイムラインや他のユーザーのツイートを取得するために使用するAPI。

のみ実装することにします。

`src/index.js` に以下のようにエンドポイントを定義します。

```javascript:src/index.js
import { Hono } from 'hono'

const app = new Hono()

// ツイート取得API
app.get('/api/tweets', async c => {
})


// ツイート投稿API
app.post('/api/tweets', async c => {
})

export default app
```

これでAPIのエンドポイントが定義できました。

### [Create a database](https://developers.cloudflare.com/d1/tutorials/build-a-comments-api/#create-a-database)
ようやくD1です。
wrangler コマンドでD1のデータベース作成を行い、作成されたデータベースとWorkersプロジェクトを紐付けるための設定(Binding)を行います。

```bash
$ wrangler d1 create d1-example
--------------------
🚧 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 'd1-example'!

Add the following to your wrangler.toml to connect to it from a Worker:
```
`wrangler.tomlfile` に、出力結果を設定します。

```toml:wrangler.tomlfile
[[ d1_databases ]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "d1-example"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
```

### [Interact with D1](https://developers.cloudflare.com/d1/tutorials/build-a-comments-api/#interact-with-d1)
`wrangler` のコマンドを使用してD1の操作をします。
```bash
$ wrangler d1 execute d1-example --command "SELECT name FROM sqlite_schema WHERE type ='table'"
🌀 Mapping SQL input into an array of statements
🌀 Parsing 1 statements
🌀 Executing on d1-example
🚣 Executed 1 command
┌───────┐
│ name  │
├───────┤
│ d1_kv │
└───────┘
```
`wrangler` からSQLファイルを渡してテーブルを作成することも可能です。チュートリアルではブログのコメントを管理するテーブルを作成していますが、Twitter似のサービスを作成することにしたのでそれを実現するテーブルを作成することにします。テーブル定義は例によってChatGPTに考えてもらいましょう。

#### Twitterのような機能をもつWebサービスを作成したいです。テーブル定義を考えてください。

:::note
TwitterのようなWebサービスを作成するためには、以下のテーブルが必要だと思われます。
1. Usersテーブル: ユーザーのプロフィール情報を格納するテーブル。必須項目は、ユーザー名、パスワード、メールアドレスなど。

2. Tweetsテーブル: ツイートの情報を格納するテーブル。必須項目は、ツイートのテキスト、投稿者のID、投稿日時など。

3. Followersテーブル: フォロー関係を格納するテーブル。必須項目は、フォローしているユーザーのID、フォローされているユーザーのIDなど。

4. Likesテーブル: いいね情報を格納するテーブル。必須項目は、いいねをしたユーザーのID、いいねを受けたツイートのIDなど。

5. Retweetsテーブル: リツイート情報を格納するテーブル。必須項目は、リツイートをしたユーザーのID、リツイートされたツイートのIDなど。

これらのテーブルを使用すれば、Twitterのような機能を実装することができます。
:::

User、Tweetsテーブルだけ作成することにしましょう。

#### Usersテーブル、Tweetsテーブルを作成するDDLを出力してください。
:::note
UsersテーブルのDDL:

```sql
CREATE TABLE Users (
  id INT PRIMARY KEY,
  username VARCHAR(255) NOT NULL,
  password VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
TweetsテーブルのDDL:
```sql
CREATE TABLE Tweets (
  id INT PRIMARY KEY,
  user_id INT NOT NULL,
  text VARCHAR(255) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES Users(id)
);
:::

もう少し注文をつけてみましょう。
#### Usersテーブル、Tweetsテーブルを作成するSQLite用のDDLを出力してください。idはAutoincrementとしてください。
:::note
```sql
CREATE TABLE Users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT NOT NULL UNIQUE,
  password TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE Tweets (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES Users(id)
);

```
上記のDDLは、SQLiteデータベースに対して、Users、Tweetsのテーブルを作成します。
Usersテーブルには、ユーザーのプロフィール情報を格納するカラムがあります。
Tweetsテーブルには、ツイートの情報を格納するカラムがあります。

それぞれのテーブルには、Primary Keyが設定されており、 Foreign Key で別のテーブルと関連付けられています。
特に、Usersテーブルのidカラムは、 AUTOINCREMENT となっており、自動的に値が振られるようになっています。

SQLiteには自動インデックスがありますので、明示的にインデックスを作成する必要はありません。
しかし、検索やソートを高速にするためには、適切なインデックスを設定することが望ましいです。
:::
Indexはデータが増えてから考えましょう。
適当に初期データも用意してSQLファイルを作成します。

```sql:shememas/users.sql
CREATE TABLE Users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT NOT NULL UNIQUE,
  password TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO Users (username, password) VALUES ("Vantiq01", "password01");
INSERT INTO Users (username, password) VALUES ("Vantiq02", "password02");
```

```sql:shememas/tweets.sql
CREATE TABLE Tweets (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES Users(id)
);

INSERT INTO Tweets (user_id, content) VALUES (1, "Vantiq01's first tweet!");
INSERT INTO Tweets (user_id, content) VALUES (2, "Vantiq02's first tweet!");
```

`wrangler` でファイルを実行し、テーブルの作成と初期データの投入を行いましょう。
```bash
$ wrangler d1 execute d1-example --file schemas/users.sql
$ wrangler d1 execute d1-example --file schemas/tweets.sql
```

テーブルが作成されていることを確認します。
```bash
$ wrangler d1 execute d1-example --command "SELECT name FROM sqlite_schema WHERE type ='table'"

🌀 Mapping SQL input into an array of statements
🌀 Parsing 1 statements
🌀 Executing on d1-example
🚣 Executed 1 command in 0.09222199767827988ms
┌─────────────────┐
│ name            │
├─────────────────┤
│ d1_kv           │
├─────────────────┤
│ Users           │
├─────────────────┤
│ sqlite_sequence │
├─────────────────┤
│ Tweets          │
└─────────────────┘
```

### [Execute SQL](https://developers.cloudflare.com/d1/tutorials/build-a-comments-api/#execute-sql)
APIでSQLを実行し結果を返すようにしてみます。 `src/index.js` に定義した`get` のエンドポイントにTweetsを取得する処理を書いていきましょう。クエリパラメータでuser_idを受け取ることにします。
```javascript:src/index.js
app.get('/api/tweets', async c => {
	const user_id = c.req.query('user_id')
	const { results } = await c.env.DB.prepare(`
    select * from Tweets where user_id = ?
  `).bind(user_id).all()
  return c.json(results)
})
```
さて、いったんローカルで動作確認してみたいと思いますがチュートリアルにはその手順がありません。
ローカルで動かすには [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/#dev) コマンドを使用します。
```bash
$ wrangler dev --local --persist
Your worker has access to the following bindings:
- D1 Databases:
  - DB: d1-example (local)
⎔ Starting a local server...
```
先程作成したテーブルはクラウド上に作成しているので、ローカルのDBにも作成します。
```bash
$ wrangler d1 execute d1-example --file schemas/users.sql --local
$ wrangler d1 execute d1-example --file schemas/tweets.sql --local
```
ローカルにテーブルとデータがある状態でローカルサーバを起動しました。APIにリクエストしてみましょう。

```bash
$ curl --location --request GET 'http://127.0.0.1:8787/api/tweets?user_id=1'
[{"id":1,"user_id":1,"content":"Vantiq01 first tweet!","created_at":"2023-01-27 03:52:46"}]%   
```
データの取得に成功しました。

### [Insert data](https://developers.cloudflare.com/d1/tutorials/build-a-comments-api/#insert-data)
次はデータの挿入APIです。
`src/index.js` に定義した`post` のエンドポイントにTweetsテーブルにデータを挿入する処理を書き、新規Tweetsを投稿するAPIを実装していきます。処理を書いていきましょう。

```javascript:src/index.js
app.post('/api/tweets', async c => {
  const { user_id, content } = await c.req.json()

  if (!user_id) return c.text("Missing user_id value for new tweet")
  if (!content) return c.text("Missing content value for new tweet")

  const { success } = await c.env.DB.prepare(`
    insert into Tweets (user_id, content) values (?, ?)
  `).bind(user_id, content).run()

  if (success) {
    c.status(201)
    return c.text("Created")
  } else {
    c.status(500)
    return c.text("Something went wrong")
  }
})
```
ローカルで実行してみます。
```bash
$ curl --location --request POST 'http://127.0.0.1:8787/api/tweets' \
--header 'Content-Type: application/json' \
--data-raw '{
    "user_id":1,
    "content":"お腹すいた"

}' 
Created
```
成功しました。`get`のAPIで、データが作成されているか確認してみましょう。
```bash
$ curl --location --request GET 'http://127.0.0.1:8787/api/tweets?user_id=1'
[{"id":1,"user_id":1,"content":"Vantiq01 first tweet!","created_at":"2023-01-27 03:52:46"},
{"id":3,"user_id":1,"content":"お腹すいた","created_at":"2023-01-27 05:33:31"}]   
```
`post`のAPIにリクエストしたTweetsが作成されています。これで作成したかった機能が揃いました。

### [Deployment](https://developers.cloudflare.com/d1/tutorials/build-a-comments-api/#deployment)
デプロイを実行してプロジェクトをCloudflareのエッジネットワークに公開します。
デプロイは非常に簡単で、`wrangler publish` を実行するだけです。
```bash
$ wrangler publish
Your worker has access to the following bindings:
- D1 Databases:
  - DB: d1-example
Total Upload: 42.64 KiB / gzip: 10.38 KiB
Uploaded d1-example (1.44 sec)
Published d1-example (4.15 sec)
  https://d1-example.xxxx.workers.dev
Current Deployment ID: xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx
```
デプロイされました。Cloudflare上のAPIにリクエストしてみましょう。
```bash
$ curl --location --request GET 'https://<your url>/api/tweets?user_id=1'
[{"id":1,"user_id":1,"content":"Vantiq01's first tweet!","created_at":"2023-01-27 03:51:25"}]
```
結果が返ってきました。正しくデプロイされていることが確認できました。


## 感想
ローカルでの動作確認やデプロイのしやすさで開発効率はかなり高いと感じました。今後の機能追加に期待しつつ、分散データベースの特徴を活かした使いみちを考えていきたいですね。