Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Durable Objects ストレージへのアクセス

最終更新 Markdown で表示Agent セットアップ

Durable Objects は、コンピュートとストレージを組み合わせた構成要素を提供する、強力なコンピュート API です。各 Durable Object は、プライベートでトランザクション性があり、強い一貫性を持つ独自のストレージを持ちます。Durable Objects の Storage API で、その Durable Object に付属するストレージへアクセスできます。

Durable Object の メモリ内状態 は、メモリから退避(evict)されない限り保持されます。受信リクエストがない非アクティブな Durable Objects は退避されることがあります。コードのデプロイ など、通常の操作でも Durable Objects が再起動し、メモリ内状態が失われます。そのため、退避や再起動後も残す必要のある状態は、Storage API でディスクに永続化してください。

ストレージへのアクセス

Storage API のメソッド は、Durable Object のコンストラクターに渡される ctx.storage で利用できます。Storage API には、SQL、ポイントインタイムリカバリ(PITR)、キーバリュー(KV)、アラーム API などがあります。

SQL API を使えるのは、SQLite ストレージバックエンドを持つ Durable Object クラスだけです。

SQLite バックエンドの Durable Object クラスを作成する

Worker の Wrangler ファイルのマイグレーションで new_sqlite_classes を使います。

{
	"migrations": [
		{
			"tag": "v1", // Should be unique for each entry
			"new_sqlite_classes": [ // Array of new classes
				"MyDurableObject"
			]
		}
	]
}
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "MyDurableObject" ]

SQL API は、Durable Object のコンストラクターに渡される ctx.storage.sql で利用できます。

SQLite バックエンドの Durable Objects では、ポイントインタイムリカバリ API も使えます。ブックマーク を使い、埋め込み SQLite データベースを過去 30 日以内の任意の時点に戻せます。

ストレージからインスタンス変数を初期化する

よくあるパターンは、初回アクセス時に 永続ストレージ から Durable Object を初期化し、インスタンス変数を設定することです。以降のアクセスは同じ Durable Object にルーティングされるため、永続ストレージを再度呼び出さずに、初期化済みの値を返せます。

import { DurableObject } from "cloudflare:workers";

export class Counter extends DurableObject {
	value: number;

	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);

		// `blockConcurrencyWhile()` ensures no requests are delivered until
		// initialization completes.
		ctx.blockConcurrencyWhile(async () => {
			// After initialization, future reads do not need to access storage.
			this.value = (await ctx.storage.get("value")) || 0;
		});
	}

	async getCounterValue() {
		return this.value;
	}
}

Durable Object のストレージを削除する

シャットダウン時にストレージが空だと、Durable Object は完全に存在しなくなります。アラーム の設定も含め、Durable Object のストレージに一度も書き込まない場合、ストレージは空のままなので、シャットダウン後はその Durable Object は存在しなくなります。

一方、アラームの設定を含め、Storage API で一度でも書き込んだ場合は、ストレージを空にするために storage.deleteAll() を明示的に呼ぶ必要があります。アラームを設定している場合は storage.deleteAlarm() も呼びます。書き込んだデータだけを消す(キーの削除やテーブルの削除など)だけでは不十分です。メタデータが残ることがあるためです。ストレージをすべて削除する唯一の方法は deleteAll() を呼ぶことです。deleteAll() を呼ぶと、その Durable Object のストレージ課金が発生しなくなります。

export class MyDurableObject extends DurableObject<Env> {
	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
	}

	// Clears Durable Object storage
	async clearDo(): Promise<void> {
		// If you've configured a Durable Object alarm
		await this.ctx.storage.deleteAlarm();

		// This will delete all the storage associated with this Durable Object instance
		// This will also delete the Durable Object instance itself
		await this.ctx.storage.deleteAll();
	}
}

SQL API の例

以降の SQL API の例では、次の SQL スキーマを使います。

import { DurableObject } from "cloudflare:workers";

export class MyDurableObject extends DurableObject {
  sql: SqlStorage
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sql = ctx.storage.sql;

    this.sql.exec(`CREATE TABLE IF NOT EXISTS artist(
      artistid    INTEGER PRIMARY KEY,
      artistname  TEXT
    );INSERT INTO artist (artistid, artistname) VALUES
      (123, 'Alice'),
      (456, 'Bob'),
      (789, 'Charlie');`
    );
  }
}

クエリ結果を行オブジェクトとして反復します。

  let cursor = this.sql.exec("SELECT * FROM artist;");

  for (let row of cursor) {
    // Iterate over row object and do something
  }

クエリ結果を行オブジェクトの配列に変換します。

  // Return array of row objects: [{"artistid":123,"artistname":"Alice"},{"artistid":456,"artistname":"Bob"},{"artistid":789,"artistname":"Charlie"}]
  let resultsArray1 = this.sql.exec("SELECT * FROM artist;").toArray();
  // OR
  let resultsArray2 = Array.from(this.sql.exec("SELECT * FROM artist;"));
  // OR
  let resultsArray3 = [...this.sql.exec("SELECT * FROM artist;")]; // JavaScript spread syntax

クエリ結果を、行の値配列の配列に変換します。

  // Returns [[123,"Alice"],[456,"Bob"],[789,"Charlie"]]
  let cursor = this.sql.exec("SELECT * FROM artist;");
  let resultsArray = cursor.raw().toArray();

  // Returns ["artistid","artistname"]
  let columnNameArray = this.sql.exec("SELECT * FROM artist;").columnNames.toArray();

クエリ結果の最初の行オブジェクトを取得します。

  // Returns {"artistid":123,"artistname":"Alice"}
  let firstRow = this.sql.exec("SELECT * FROM artist ORDER BY artistname DESC;").toArray()[0];

クエリ結果がちょうど 1 行かどうかを確認します。

  // returns error
  this.sql.exec("SELECT * FROM artist ORDER BY artistname ASC;").one();

  // returns { artistid: 123, artistname: 'Alice' }
  let oneRow = this.sql.exec("SELECT * FROM artist WHERE artistname = ?;", "Alice").one()

返されるカーソルの動作:

  let cursor = this.sql.exec("SELECT * FROM artist ORDER BY artistname ASC;");
  let result = cursor.next();
  if (!result.done) {
    console.log(result.value); // prints { artistid: 123, artistname: 'Alice' }
  } else {
    // query returned zero results
  }

  let remainingRows = cursor.toArray();
  console.log(remainingRows); // prints [{ artistid: 456, artistname: 'Bob' },{ artistid: 789, artistname: 'Charlie' }]

返されるカーソルと raw() イテレーターは、同じクエリ結果を反復します。

  let cursor = this.sql.exec("SELECT * FROM artist ORDER BY artistname ASC;");
  let result = cursor.raw().next();

  if (!result.done) {
    console.log(result.value); // prints [ 123, 'Alice' ]
  } else {
    // query returned zero results
  }

  console.log(cursor.toArray()); // prints [{ artistid: 456, artistname: 'Bob' },{ artistid: 789, artistname: 'Charlie' }]

sql.exec().rowsRead():

  let cursor = this.sql.exec("SELECT * FROM artist;");
  cursor.next()
  console.log(cursor.rowsRead); // prints 1

  cursor.toArray(); // consumes remaining cursor
  console.log(cursor.rowsRead); // prints 3

TypeScript とクエリ結果

TypeScript の 型パラメーター で結果の型を指定すると、クエリ結果を反復するときに型ヒントと型チェックを利用できます。

型は、列名(string)と列の型を表す TypeScript の Record 型の形に合わせる必要があります。列の型は有効な SqlStorageValue、つまり ArrayBuffer | string | number | null のいずれかである必要があります。

例:

type User = {
	id: string;
	name: string;
	email_address: string;
	version: number;
};

この型を、sql.exec() 呼び出しの型パラメーターとして渡せます。

// The type parameter is passed between angle brackets before the function argument:
const result = this.ctx.storage.sql
	.exec<User>(
		"SELECT id, name, email_address, version FROM users WHERE id = ?",
		user_id,
	)
	.one();
// result will now have a type of "User"

// Alternatively, if you are iterating over results using a cursor
let cursor = this.sql.exec<User>(
	"SELECT id, name, email_address, version FROM users WHERE id = ?",
	user_id,
);
for (let row of cursor) {
	// Each row object will be of type User
}

// Or, if you are using raw() to convert results into an array, define an array type:
type UserRow = [
	id: string,
	name: string,
	email_address: string,
	version: number,
];

// ... and then pass it as the type argument to the raw() method:
let cursor = sql
	.exec(
		"SELECT id, name, email_address, version FROM users WHERE id = ?",
		user_id,
	)
	.raw<UserRow>();

for (let row of cursor) {
	// row is of type User
}

より複雑な型を含め、任意の結果型の形を表せます。複数テーブルを JOIN する場合は、クエリ結果を反映した型を組み立てられます。

SQLite のインデックス

よく検索するテーブルと絞り込みに使う列にインデックスを作ると、スキャンするデータ量が減り、同時にクエリ性能も上がります。読み取りが多いワークロード(最も一般的)では、特に効果があります。インデックスが参照する列への書き込みは、インデックス更新のために少なくとも 1 行分の追加書き込みが発生します。ただし、インデックスの効果で読み取る行が減ることで、通常は相殺されます。

Durable Objects の SQL と D1 の比較

Cloudflare Workers には、SQLite をバックエンドにしたサーバーレスデータベース製品 D1 があります。Durable Objects の SQLite と D1 をどう比較すればよいでしょうか。

D1 はマネージドなデータベース製品です。

D1 は、アプリケーションサーバーがネットワーク経由でデータベースと通信する、開発者にとってなじみのある構成に合います。アプリケーションサーバーは通常 Workers です。ただし D1 は HTTP API による外部(Worker 以外)からのアクセスにも対応しており、D1 向けの サードパーティツール の利用につながります。

D1 は必要な機能が最初から揃った機能セットを目指しています。上記の HTTP API、データベーススキーマ管理データのインポート / エクスポートデータベースクエリインサイト が含まれます。

D1 ではアプリケーションコードと SQL データベースのクエリが同じ場所にないため、アプリケーションの性能に影響することがあります。D1 で性能が気になる場合、Workers の Smart Placement を使うと、D1 を含む Worker が通信するすべての対象を考慮し、Worker リクエスト全体のレイテンシが小さくなる場所で Worker を動的に実行できます。

Durable Objects の SQLite は、分散システム向けの、より低レベルのコンピュートとストレージの構成要素です。

設計上、Durable Objects へのアクセスは Workers からのみです。

Durable Objects は手間が増えますが、そのぶん柔軟性と制御が得られます。Durable Objects では、別の場所で動く 2 つのコードを実装する必要があります。インターネットからの受信リクエストを一意の Durable Object へルーティングするフロントエンド Worker と、SQLite データベースと同じマシンで動く Durable Object 本体です。どこで何を動かすかを選べます。アプリケーションのビジネスロジックの一部をデータベースのすぐ隣で動かすと、利点がある場合もあります。

Durable Objects の SQLite では、D1 に最初から付いているデータベースツールの一部を、自分で作る必要があることもあります。

SQL クエリの料金と制限は、D1(料金制限)と Durable Objects の SQLite(料金制限)で同じになる想定です。

関連リソース

役に立ちましたか?