😊

Cloudflare Workers から D1 を操作する

2023/02/18に公開
11

D1はCloudflareが提供するエッジデータベースです。実体はSQL Liteです。Cloudflareにはリージョンの概念がなく285以上のすべてのエッジがマスターとなります。これによりマルチマスター構成のリレーショナルデータベースが簡単に作れます。
執筆時点でアルファ版であり、商用環境での利用が推奨されていません。また今後API仕様の変更なども予想されています。

この記事では前半はCloudflareが提供しているチュートリアルの内容を少しだけ書き換えたものです。執筆時点でなぜかチュートリアル通りに動作しなかったため(恐らくアルファ版で不安定な部分があるため)手順などを書き換えています。
後半はその他いろいろ試してみるコーナーです。

1.Wrangler環境セットアップ
この記事を参考に環境をセットアップしてください。タイトルにWindowsと入っていますが、Macでも基本手順は同じです。

2.プロジェクトのinit
wrangler init firstd1project -yを実行します。
-yオプションは対話で求められるオプションに全てYesで答えるオプションです。これによりプロジェクトはJavaScriptではなくTypeScriptで作成されます。
(20231021 updated)
TypeScritpやGitはYes/Noを聞いてくるようなのでTS=Yes,Git=Noにしてください。
Do you want to deploy your application?=Yesとして、まずはHello Worldの表示まで進めます。

cd firstd1projectに移動し、wrangler d1 create firstd1を実行します。
出力された以下の文字列をコピーします。

[[ d1_databases ]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "firstd1"
database_id = "6e021714-26af-4a53-8d24-890f9be20156"

(database_idは皆さん固有に値になります)
これをwrangler.tomlに貼り付けます。
wrangler.tomlと同じフォルダにtest.sqlというファイルを作成し以下の値を書き込み保存します

CREATE TABLE Customers (CustomerID INT, CompanyName TEXT, ContactName TEXT, PRIMARY KEY (`CustomerID`));
INSERT INTO Customers (CustomerID, CompanyName, ContactName) VALUES (1, 'Alfreds Futterkiste', 'Maria Anders'), (4, 'Around the Horn', 'Thomas Hardy'), (11, 'Bs Beverages', 'Victoria Ashworth'), (13, 'Bs Beverages', 'Random Name');

次に以下のコマンドを実行しローカル環境のD1データベースに上記SQL(テーブル作成と値の書き込み)を行っておきます。
wrangler d1 execute firstd1 --local --file=./test.sql

実行が完了したら以下を実行し、SQLが正しく生成されたか確認しておきます。
wrangler d1 execute firstd1 --local --command='SELECT * FROM Customers'

正しく値が入っています。

3.Workersの設定
それではこれからWorkersからD1を呼び出すスクリプトを作成します。今回は上記で説明した通りTypeScriptを用います。
以下の値でsrc/index.tsを置換します。

export default {
  async fetch(request: Request, env: Env) {
    const { pathname } = new URL(request.url);

    if (pathname === "/api/beverages") {
      const { results } = await env.DB.prepare(
        "SELECT * FROM Customers WHERE CompanyName = ?"
      )
        .bind("Bs Beverages")
        .all();
      return Response.json(results);
    }

    return new Response(
      "Call /api/beverages to see everyone who works at Bs Beverages"
    );
  },
};

(すごい余談ですが.tsといえばMPEG2-TS形式の映像ファイルなのでファイルアイコンが気になります)

4.テストとデプロイ
それではこれから動作検証ですがまずはローカル環境で実行してみます。以下のコマンドを実行します。
wrangler dev --local
"b"を押してブラウザを立ち上げてみましょう。
http://127.0.0.1:8787/api/beveragesにアクセスするとデータベースの中身がJSONで表示されます。

ここで少し簡単にWorkersのソースを解説してみます。先ほどWrangler.tomlでD1のバインディング設定を行いましたので、Workersからは特殊な設定不要で"DB"をD1として認識します。公式サンプルでは以下の宣言が含まれていますが、なくても動作します。

export interface Env {
  DB: D1Database;
}

if (pathname === "/api/beverages") {の指定により"/api/beverages"にアクセスされたときのみ以下のSQLが実行されます。

const { results } = await env.DB.prepare(
        "SELECT * FROM Customers WHERE CompanyName = ?"
      )
      .bind("Bs Beverages")

ここでいう"DB.prepare"の部分がCloudflareにより準備されているD1のAPIになります。このSQL実行結果がJSONとして"result"に格納され表示ます。

ではいよいよデプロイです。まず"x"を押してローカル環境の実行を停止させたのち以下を実行します。
wrangler publish
表示されるURLをコピーしておいてください。

Workersのマネージメントコンソールで"firstd1project"が無事確認できていれば成功です。

Settings→Variablesを見るとKVやR2の時に確認できたD1のバインディング設定を行う箇所がないようです。これは今後改善されているはずですが、wrangler.tomlで行った設定は正しく入っているはずですのであとでテストします。

次にD1のマネージメントコンソールで"firstd1"を確認してみてください。残念ながら先ほどローカルで作成したテーブルやINSERTされた値が反映されていないようです。先ほどは--localオプションを付けていたからクラウド側には反映されていません。(この辺り公式ドキュメントは少し説明不足のようです)

このままではテストが行えません。(NULLが戻ってきますが、WorkersのスクリプトはNULL処理をしていないのでエラーとなります。)ここからがエバンジェリストの腕の見せ所です、知らんけど。

以下を実行します。
wrangler d1 execute DB --file test.sql
D1マネージメントコンソールを見るとテーブルとデータが増えていることがわかります。


では先ほどコピーしておいたURLにアクセスしてください。後ろに"api/beverages"を付けてくださいね。無事JSONで値が表示されます。

ここからオリジナルでいろいろやってみるコーナーです。
5. 見た目のお化粧
上記のコードだと生JSONが表示されるためテストが少し大変です。index.tsのコードを以下に変更しておきます。

export default {
  async fetch(request: Request, env: Env) {
    const { pathname } = new URL(request.url);

    if (pathname === "/api/beverages") {
      const { results } = await env.DB.prepare(
        "SELECT * FROM Customers WHERE CompanyName = ?"
      )
        .bind("Bs Beverages")
        .all();

      const tableHtml = createTableHtml(results); // JSONからHTMLテーブルを生成する
      return new Response(tableHtml, { headers: { "content-type": "text/html" } });
    }

    return new Response(
      "Call /api/beverages to see everyone who works at Bs Beverages"
    );
  },
};

function createTableHtml(data) {
  let tableHtml = "<table><thead><tr>";

  // ヘッダー行を作成する
  Object.keys(data[0]).forEach((key) => {
    tableHtml += `<th>${key}</th>`;
  });

  tableHtml += "</tr></thead><tbody>";

  // データ行を作成する
  data.forEach((row) => {
    tableHtml += "<tr>";
    Object.values(row).forEach((value) => {
      tableHtml += `<td>${value}</td>`;
    });
    tableHtml += "</tr>";
  });

  tableHtml += "</tbody></table>";

  return tableHtml;
}


綺麗になりました!

6.Backupの取得
(20231021 updates) Betaバージョンリリース時にこのコマンドはなくなりました。
D1は1時間に1回自動でバックアップが取得されますが、開発環境などでは手動でバックアップを取得したいときもあります。その時は以下のコマンドを実行してください
wrangler d1 backup create DB

wrangler d1 backup download DB <BACKUP_ID>を実行すればバックアップをローカルに保存しておくこともできます。

バックアップのリストアは以下のコマンドです。
wrangler d1 backup restore DB <BACKUP_ID>
ローカルにDLしたバックアップは戻す際には一度SQLコマンドに変更しておく必要があります。以下のコマンドを実行すれば戻せます。
sqlite3 db_dump.sqlite3 .dump > db.sql. あとはwrangler でwrangler d1 execute DB --file db.sql`を実行すれば完了です。

(20231021 updates)
上記の機能がなくなった代わりに、TimeTravelというBackup機能がついています。
過去30日であれば時間を指定することでBackupの復元が可能です。

wrangler d1 time-travel info <BIND DB Name>

このコマンドで今のDBのブックマークを取得できます。ブックマークはDBをRestoreするポイントになります。
wrangler d1 time-travel info YOUR_DATABASE

🚧 Time Traveling...
⚠️ The current bookmark is '00000085-0000024c-00004c6d-8e61117bf38d7adb71b934ebbf891683'
⚡️ To restore to this specific bookmark, run:
 `wrangler d1 time-travel restore <BIND DB Name> --bookmark=00000085-0000024c-00004c6d-8e61117bf38d7adb71b934ebbf891683`

またブックマークを用いず時間指定で戻す場合(ブックマークが無くてもRestoreは可能です)以下のコマンドになります。

wrangler d1 time-travel restore <BIND DB Name> --timestamp=UNIX_TIMESTAMP

現在ブランチ機能は存在せず、現行のDBを上書きしますので注意してください。ただし以下のコマンドが出力されますので、Rstore直前に状態に戻すことは可能です。

To undo this operation, you can restore to the previous bookmark: 00000085-ffffffff-00004c6d-2510c8b03a2eb2c48b2422bb3b33fad5

7.Javascriptでやってみる
別の記事ではKVR2を触りましたが、それらはJavascriptベースでした。一方D1は公式ドキュメントはTypescriptベースでJavascriptのサンプルがありません。悩んでいたところこの記事のコメント欄でyusukebさんから単純なサンプルをいただきましたのでやってみます。
まずは再度wrangler init hogegoeで新しいプロジェクトを作成し、上記で使っていたwrangler.tomlの[[ d1_databases ]]から下の行をコピーして保存します。

その後src/index.jsを自身のindex.jsに保存してwrangler publishを実行してください。JSONでD1の中身が表示されれば成功です。
念のためいかに転載しておきます。

export default {
  async fetch(request, env) {
    const { pathname } = new URL(request.url)

    if (pathname === '/api/beverages') {
      const { results } = await env.DB.prepare('SELECT * FROM Customers WHERE CompanyName = ?')
        .bind('Bs Beverages')
        .all()
      return Response.json(results)
    }

    return new Response('Call /api/beverages to see everyone who works at Bs Beverages')
  },
}

その後例えば以下を実行してみてください。

export default {
  async fetch(request, env) {

	const { duration } = (await env.DB.prepare('Select * from Customers').all()).meta
	return Response.json(duration)
 },
}

クエリの実行時間が取れるはずです。

8.執筆時点での使用や制限など
列や行に制限はありませんが、データサイズはトータルで500MB未満である必要があります。(有償版で2GB)。バックアップは1時間に1回取得されていますが、その間1-2秒のアクセスブロックが発生します。
主な制限はこちら出す。
https://developers.cloudflare.com/d1/platform/limits/

それから、KVやR2と異なり、WorkersエディタではD1はまだ動作しませんので気を付けてください。

Discussion

yusukebeyusukebe

次にD1のマネージメントコンソールで"firstd1"を確認してみてください。残念ながら先ほどローカルで作成したテーブルやINSERTされた値が反映されていないようです。(もし反映されている方がいたら次のコマンドは実行不要です)恐らくアルファ版の制限と思われます。

これは単純にローカルでやってたからで、ローカルでの作業はダッシュボードには反映されないからですね。なので最初から--localをつけないでやって、かつ、wrangler.tomlではpreview_database_idを指定して本番を参照させるのもいいかもしれません。

kameoncloudkameoncloud

ありがとうございます。となると
https://developers.cloudflare.com/d1/get-started/
の手順が不足、ないしは説明不足ってことですかね。
記事の内容修正しておきます!

ところでなんでD1は公式サンプルではTypeScript版のみでJavaScriptはhonoベースしかないのでしょうね。hono使わず素のJavaScriptで動作させる方法に挑戦しているもののうまくいかず苦戦中です。

yusukebeyusukebe

素のJavaScriptはCloudflareの中のデベロッパーも使わない方針なんじゃないっすかね。Hono使ってるサンプルもTypeScriptです。公式のテンプレートのも最近のはTypeScriptしかないです。
https://github.com/cloudflare/workers-sdk/tree/main/templates
Wranglerがトランスパイル意識せずに直接TypeScriptを読めるのでそうなってると思います。

kameoncloudkameoncloud

ありがとうございます!実は今シンガポール(APJCのヘッドクォーター)に来ており、識者にいろいろ聞いて回ってるんですが、やはりみんなJavascript? しらんなぁ。てな感じでした。
もう少し調べてみます!

kameoncloudkameoncloud

7.を追記してみました。意にそぐわなければ申し訳ありません!

yusukebeyusukebe

constの指定やrun()がall()など若干異なります。

これはそれぞれのサンプルでやってることが違うだけで、JavaScriptでもTypeScriptでも同じだと思います。

kameoncloudkameoncloud

run()だといろいろやっても動作しなかったんですが、まだ何か足りないのですかね。
修正しておきます!ありがとうございます。

yusukebeyusukebe

run()はドキュメントにあるように、SELECTでは使えないですね。例えばINSERTなら使えます。

const results = await env.DB.prepare(
  'INSERT INTO Customers (CustomerID, CompanyName, ContactName) VALUES (?, ?, ?)'
)
  .bind(123, 'foo', 'bar')
  .run()

そして、そもそも

ただしここのドキュメントはJavascriptではそのまま動作しないことに注意してください。

ここで指定しているドキュメントのコードはJavaScriptで書かれています。ですので、TypeScriptでも型がないですが動きますし、JavaScriptでも動きます。

kameoncloudkameoncloud

"Runs the query/queries, but returns no results. Instead, run() returns the metrics only. Useful for write operations like UPDATE, DELETE or INSERT."の部分がSELECTでは動作しないってことなのか。理解できていないので余計な部分消しました。ありがとうございます。