INTEGRITY Документация

Доступ к хранилищу Durable Objects

Durable Objects являются мощным API для вычислений, который предоставляет строительный блок «вычисления + хранилище». У каждого Durable Object есть собственное приватное, транзакционное и строго консистентное хранилище. Durable Objects Storage API предоставляет доступ к прикреплённому хранилищу Durable Object.

У Durable Object состояние в памяти сохраняется до тех пор, пока Durable Object не будет вытеснен из памяти. Неактивные Durable Objects без входящих запросов могут быть вытеснены. Существуют обычные операции, например развертывания кода которые приводят к перезапуску Durable Object и потере состояния, хранившегося в памяти. По этой причине для состояния, которое должно сохраняться при вытеснении или перезапуске Durable Object, следует использовать Storage API для надёжного сохранения на диске.

Доступ к хранилищу

Методы Storage API доступны на ctx.storage параметр, передаваемый в конструктор Durable Object. Storage API включает несколько групп методов: SQL, восстановление на заданный момент времени (PITR), хранилище ключ-значение (KV) и API для alarm.

Доступ к SQL API имеют только классы Durable Object с хранилищем на основе SQLite.

Создание класса Durable Object на основе SQLite

Используйте new_sqlite_classes в миграции в файле Wrangler вашего Worker:

{
	"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 доступен в ctx.storage.sql параметр, передаваемый в конструктор Durable Object.

Durable Objects на основе SQLite также предоставляют point-in-time recovery API, который использует точки восстановления (bookmarks), которые позволяют восстановить встроенную базу данных SQLite объекта Durable Object на любой момент времени за последние 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 (включая установку alarms), то хранилище останется пустым, и поэтому Durable Object перестанет существовать после завершения работы.

Однако если вы когда-либо выполняете запись с использованием Storage API, включая установку будильников, то вам нужно явно вызвать storage.deleteAll() чтобы очистить хранилище и storage.deleteAlarm(), если вы настроили alarm. Недостаточно просто удалить записанные вами данные, например удалить ключ или таблицу, поскольку часть метаданных может остаться. Единственный способ полностью очистить хранилище: вызвать 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];

Проверьте, что результат запроса содержит ровно одну строку:

  // 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 параметры типа ↗ чтобы задать тип для результатов и воспользоваться подсказками типов и проверками при переборе результатов запроса.

Ваш тип должен соответствовать структуре TypeScript Запись ↗ тип, представляющий имя (string) столбца и тип столбца. Тип столбца должен быть допустимым 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) дополнительную запись для обновления индекса, но это обычно компенсируется сокращением количества считываемых строк благодаря преимуществам индекса.

SQL в Durable Objects и D1

Cloudflare Workers предлагает serverless базу данных на основе SQLite: D1. Как следует сравнивать SQLite в Durable Objects и D1?

D1 является управляемым продуктом для баз данных.

D1 вписывается в привычную для разработчиков архитектуру, где серверы приложений взаимодействуют с базой данных по сети. Обычно в роли серверов приложений выступают Workers, однако D1 также поддерживает внешний доступ без Worker через HTTP API ↗, который помогает раскрыть сторонние инструменты поддержка D1.

D1 стремится предложить набор возможностей «из коробки», включая упомянутый выше HTTP API, управление схемой базы данных, импорт и экспорт данных, а также аналитика запросов к базе данных.

В D1 код вашего приложения и SQL-запросы к базе данных не находятся в одном месте, что может влиять на производительность приложения. Если производительность D1 вызывает опасения, в Workers есть Smart Placement чтобы динамически запускать ваш Worker в оптимальном месте и снижать общую задержку запросов Worker с учетом всех сервисов, с которыми он взаимодействует, включая D1.

SQLite в Durable Objects представляет собой низкоуровневый строительный блок вычислений с хранилищем для распределённых систем.

Durable Objects по замыслу доступны только из Workers.

Durable Objects требуют немного больше усилий, но взамен дают больше гибкости и контроля. При работе с Durable Objects нужно реализовать две части кода, которые выполняются в разных местах: Worker на переднем плане, направляющий входящие запросы из интернета к нужному Durable Object, и сам Durable Object, работающий на той же машине, что и база данных SQLite. Вы сами решаете, что где выполняется, и в некоторых случаях приложению выгодно выполнять часть бизнес-логики прямо рядом с базой данных.

При использовании SQLite в Durable Objects вам может понадобиться самостоятельно реализовать часть инструментов для работы с базой данных, которые в D1 доступны из коробки.

Тарификация и лимиты SQL-запросов должны быть одинаковыми для D1 (цены, ограничения) и SQLite в Durable Objects (цены, ограничения).