Cloudflare Workers から D1 を操作する
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でやってみる
別の記事ではKVやR2を触りましたが、それらは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秒のアクセスブロックが発生します。
主な制限はこちら出す。
それから、KVやR2と異なり、WorkersエディタではD1はまだ動作しませんので気を付けてください。
Discussion
これは単純にローカルでやってたからで、ローカルでの作業はダッシュボードには反映されないからですね。なので最初から
--local
をつけないでやって、かつ、wrangler.toml
ではpreview_database_id
を指定して本番を参照させるのもいいかもしれません。ありがとうございます。となると
の手順が不足、ないしは説明不足ってことですかね。記事の内容修正しておきます!
ところでなんでD1は公式サンプルではTypeScript版のみでJavaScriptはhonoベースしかないのでしょうね。hono使わず素のJavaScriptで動作させる方法に挑戦しているもののうまくいかず苦戦中です。
素のJavaScriptはCloudflareの中のデベロッパーも使わない方針なんじゃないっすかね。Hono使ってるサンプルもTypeScriptです。公式のテンプレートのも最近のはTypeScriptしかないです。 Wranglerがトランスパイル意識せずに直接TypeScriptを読めるのでそうなってると思います。
ありがとうございます!実は今シンガポール(APJCのヘッドクォーター)に来ており、識者にいろいろ聞いて回ってるんですが、やはりみんなJavascript? しらんなぁ。てな感じでした。
もう少し調べてみます!
これでいいんじゃないっすかね。
ただ、せっかく TypeScriptでかけるのにJavaScriptで書くのはもったいない気がします。
ありがとうございます。やってみます!
7.を追記してみました。意にそぐわなければ申し訳ありません!
これはそれぞれのサンプルでやってることが違うだけで、JavaScriptでもTypeScriptでも同じだと思います。
run()だといろいろやっても動作しなかったんですが、まだ何か足りないのですかね。
修正しておきます!ありがとうございます。
run()
はドキュメントにあるように、SELECT
では使えないですね。例えばINSERT
なら使えます。そして、そもそも
ここで指定しているドキュメントのコードはJavaScriptで書かれています。ですので、TypeScriptでも型がないですが動きますし、JavaScriptでも動きます。
"Runs the query/queries, but returns no results. Instead, run() returns the metrics only. Useful for write operations like UPDATE, DELETE or INSERT."の部分がSELECTでは動作しないってことなのか。理解できていないので余計な部分消しました。ありがとうございます。