INTEGRITY Dokumentace

Přístup k úložišti Durable Objects

Durable Objects jsou výkonné výpočetní API, které poskytuje stavební prvek propojující výpočet s úložištěm. Každý Durable Object má vlastní soukromé, transakční a silně konzistentní úložiště. Durable Objects Storage API poskytuje přístup k připojenému úložišti objektu Durable Object.

Durable Objectu stav v paměti se zachová, dokud není Durable Object odstraněn z paměti. Neaktivní Durable Objects bez příchozích požadavků mohou být z paměti odstraněny. Mezi běžné operace patří například nasazení kódu kvůli kterým se Durable Objects restartují a ztratí stav uložený v paměti. Z těchto důvodů byste měli k trvalému uložení stavu na disk, který má přežít odstranění nebo restart Durable Objects, používat Storage API.

Přístup k úložišti

Metody Storage API jsou dostupné na ctx.storage parametr předaný konstruktoru Durable Object. Storage API nabízí několik metod, včetně SQL, point-in-time recovery (PITR), key-value (KV) a alarm API.

K SQL API mají přístup pouze třídy Durable Object s úložištěm SQLite.

Vytvoření třídy Durable Object podporované SQLite

Použijte new_sqlite_classes u migrace v souboru Wrangler vašeho Workeru:

{
	"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 je dostupný na ctx.storage.sql parametr předaný konstruktoru Durable Object.

Durable Objects podporované SQLite také nabízejí API pro obnovení k určitému okamžiku, který používá záložky, které umožňují obnovit vloženou databázi SQLite objektu Durable Object k libovolnému okamžiku za posledních 30 dní.

Inicializace instančních proměnných z úložiště

Běžným postupem je inicializovat Durable Object z trvalé úložiště a nastavit proměnné instance při prvním přístupu. Protože další přístupy jsou směrovány na stejný Durable Object, je pak možné vracet již inicializované hodnoty bez dalších volání do trvalého úložiště.

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;
	}
}

Odstranění úložiště Durable Object

Durable Object zcela zanikne, pokud je při vypnutí jeho úložiště prázdné. Pokud do úložiště Durable Object vůbec nezapisujete (včetně nastavení alarmy), zůstane úložiště prázdné a Durable Object po vypnutí přestane existovat.

Pokud však někdy provedete zápis pomocí Storage API, včetně nastavování alarmů, pak musíte explicitně zavolat storage.deleteAll() pro vyprázdnění úložiště a storage.deleteAlarm() pokud jste nakonfigurovali alarm. Nestačí pouze smazat konkrétní data, která jste zapsali, například smazáním klíče nebo tabulky, protože některá metadata mohou zůstat zachována. Jediný způsob, jak odstranit veškeré úložiště, je zavolat deleteAll(). Volání deleteAll() zajišťuje, že Durable Object nebude účtován za úložiště.

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();
	}
}

Příklady SQL API

SQL API příklady níže používají následující SQL schéma:

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');`
    );
  }
}

Iterace přes výsledky dotazu jako řádkové objekty:

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

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

Převod výsledků dotazu na pole objektů řádků:

  // 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

Převod výsledků dotazu na pole polí hodnot řádků:

  // 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();

Získání objektu prvního řádku z výsledků dotazu:

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

Zkontrolujte, zda výsledek dotazu obsahuje přesně jeden řádek:

  // 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()

Chování vráceného kurzoru:

  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' }]

Vrácený kurzor a raw() iterátor procházejí stejné výsledky dotazu:

  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 a výsledky dotazů

Můžete použít TypeScript typové parametry ↗ pro zadání typu výsledků, díky čemuž můžete využít nápovědu a kontrolu typů při procházení výsledků dotazu.

Váš typ musí odpovídat struktuře TypeScript Záznam ↗ reprezentující název (string) sloupce a typ sloupce. Typ sloupce musí být platný SqlStorageValue: jedna z ArrayBuffer | string | number | null.

Například,

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

Tento typ pak lze předat jako parametr typu do sql.exec() volání:

// 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
}

Tvar libovolného typu výsledku můžete reprezentovat podle potřeby, včetně složitějších typů. Pokud provádíte JOIN napříč více tabulkami můžete sestavit typ, který odráží výsledky vašich dotazů.

Indexy v SQLite

Vytvoření indexů pro nejčastěji dotazované tabulky a filtrované sloupce snižuje množství procházených dat a zároveň zlepšuje výkon dotazů. Pokud máte zátěž s převahou čtení (nejběžnější případ), přináší to výraznou výhodu. Zápis do sloupců, na které odkazuje index, si vyžádá alespoň jeden (1) další zapsaný řádek kvůli aktualizaci indexu, což je ale obvykle vyváženo nižším počtem přečtených řádků díky přínosům indexu.

SQL v Durable Objects oproti D1

Cloudflare Workers nabízí serverless databázový produkt založený na SQLite, D1. Jak byste měli porovnat SQLite v Durable Objects a D1?

D1 je spravovaný databázový produkt.

D1 zapadá do architektury, kterou vývojáři dobře znají: aplikační servery komunikují s databází po síti. Aplikačními servery jsou obvykle Workers, D1 ale podporuje i externí přístup mimo Worker prostřednictvím HTTP API ↗, což pomáhá odemknout nástroje třetích stran podpora pro D1.

D1 usiluje o ucelenou sadu funkcí typu „vše v jednom“, včetně výše uvedeného HTTP API, správa schématu databáze, import a export dat, a přehledy databázových dotazů.

U D1 nejsou kód aplikace a SQL dotazy do databáze umístěny na stejném místě, což může ovlivnit výkon aplikace. Pokud pro vás výkon D1 hraje roli, Workers nabízí Smart Placement aby dynamicky spouštělo váš Worker v nejvhodnější lokalitě a snížilo tak celkovou latenci požadavků Workeru, s ohledem na vše, s čím váš Worker komunikuje, včetně D1.

SQLite v Durable Objects je nízkoúrovňový stavební prvek výpočtu s úložištěm pro distribuované systémy.

Durable Objects jsou záměrně přístupné pouze z Workers.

Durable Objects vyžadují trochu více úsilí, ale na oplátku vám dávají větší flexibilitu a kontrolu. U Durable Objects musíte implementovat dva kusy kódu, které běží na různých místech: front-endový Worker, jenž směruje příchozí požadavky z internetu na konkrétní Durable Object, a samotný Durable Object, který běží na stejném stroji jako databáze SQLite. Sami si volíte, co poběží kde, a může se stát, že vaší aplikaci prospěje, když část obchodní logiky poběží přímo vedle databáze.

U SQLite v Durable Objects si možná budete muset sami vytvořit část databázových nástrojů, které u D1 dostanete rovnou k dispozici.

Ceny a limity SQL dotazů mají být shodné mezi D1 (ceny, limity) a SQLite v Durable Objects (ceny, limity).