跳转到内容
搜索文档

SQLite 支持的 Durable Object 存储

最后更新 查看 MarkdownAgent 设置

Durable Object Storage API 允许 Durable Objects 访问事务性且强一致的存储。Durable Object 的附加存储对其唯一实例私有,其他对象无法访问。

Durable Object Storage API 提供多种方法,包括 SQL、时间点恢复(PITR)、键值(KV)和 alarm API。可用 API 方法取决于 Durable Objects 类的存储后端,即 SQLiteKV

方法 1 SQLite 支持的 Durable Object 类 KV 支持的 Durable Object 类
SQL API
PITR API
同步 KV API 2, 3
异步 KV API 3
Alarms API

脚注

1 每个方法都隐式包装在事务中,使其结果具有原子性,并与所有其他存储操作隔离,即使访问多个键值对也是如此。

2get()put()delete()list() 这样的 KV API 方法将数据存储在隐藏的 SQLite 表 __cf_kv 中。请注意,列出所有表时可以查看此表,但无法通过 SQL API 访问其内容。

3 SQLite 支持的 Durable Objects 还使用 ctx.storage.kv同步 KV API 方法,而 KV 支持的 Durable Objects 仅提供异步 KV API 方法

访问存储

Durable Objects 通过 DurableObjectStorage 接口访问 Storage API,并通过 DurableObjectState::storage 属性访问。通常通过传递给 Durable Object 构造函数的 ctx 参数的 this.ctx.storage 访问。

以下代码片段展示如何使用 Durable Object Storage API 存储和检索数据。

export class Counter extends DurableObject {
	constructor(ctx, env) {
		super(ctx, env);
	}

	async increment() {
		let value = (await this.ctx.storage.get("value")) || 0;
		value += 1;
		await this.ctx.storage.put("value", value);
		return value;
	}
}
export class Counter extends DurableObject {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
  }

    async increment(): Promise<number> {
      let value: number = (await this.ctx.storage.get('value')) || 0;
      value += 1;
      await this.ctx.storage.put('value', value);
      return value;
    }

}
from workers import DurableObject

class Counter(DurableObject):
  def __init__(self, ctx, env):
    super().__init__(ctx, env)

  async def increment(self):
    value = (await self.ctx.storage.get('value')) or 0
    value += 1
    await self.ctx.storage.put('value', value)
    return value

JavaScript 是一种单线程和事件驱动的编程语言。这意味着 JavaScript 运行时默认允许请求相互交错,可能导致并发 bug。Durable Objects 运行时使用 input gateoutput gate 的组合,在执行存储操作时避免此类并发 bug。在我们的博客文章中了解更多。

SQL API

SqlStorage 接口封装修改 Durable Object 内嵌入式 SQLite 数据库的方法。SqlStorage 接口可通过 DurableObjectStorage 类的 sql 属性 访问。

例如,使用 sql.exec() 用户可以创建表并插入行。

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');
    `);
  }
}
from workers import DurableObject

class MyDurableObject(DurableObject):
  def __init__(self, ctx, env):
    super().__init__(ctx, env)
    self.sql = ctx.storage.sql

    self.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');
    """)

Durable Objects 支持 支持 SQLite 扩展的子集,以提供额外功能,包括:

有关支持函数的完整列表,请参阅源代码

exec

exec(query: string, ...bindings: any[]): SqlStorageCursor

参数 (Parameters)

  • query: string
    • 要执行的 SQL 查询字符串。query 可以包含用于参数绑定的 ? 占位符。在 query 中可以执行由分号分隔的多个 SQL 语句。如果包含多个 SQL 语句,任何参数绑定仅应用于 query 中的最后一个 SQL 语句,并且返回的游标也仅针对最后一个 SQL 语句。
  • ...bindings: any[]可选 (Optional)
    • query 中的 ? 占位符相对应的可选的可变数量的参数。

返回值 (Returns)

用于将查询行结果作为对象进行迭代的游标 (SqlStorageCursor)。SqlStorageCursor 是一个 JavaScript Iterable,支持使用 for (let row of cursor) 进行迭代。SqlStorageCursor 也是一个 JavaScript Iterator,支持使用 cursor.next() 进行迭代。

SqlStorageCursor 支持以下方法:

  • next()
    • 返回一个表示游标下一个值的对象。返回的对象具有符合 JavaScript Iterator 规范的 donevalue 属性。当存在下一个值时,done 设置为 false,且 value 设置为查询结果中的下一行对象。当整个游标被消费完毕时,done 设置为 true,且不设置 value
  • toArray()
    • 迭代剩余的游标值并返回返回的行对象的数组。
  • one()
    • 如果查询结果恰好只有一行,则返回行对象。如果查询结果有零行或多于一行,one() 将抛出异常。
  • raw(): Iterator
    • 返回针对相同查询结果的 Iterator,其中每行作为列值数组(无列名)而不是对象。
    • 返回的 Iterator 支持上述 next()toArray() 方法。
    • 返回的游标和 raw() 迭代器迭代相同的查询结果,并且可以组合使用。例如:
let cursor = this.sql.exec("SELECT * FROM artist ORDER BY artistname ASC;");
let rawResult = cursor.raw().next();

if (!rawResult.done) {
  console.log(rawResult.value); // 输出 [ 123, 'Alice' ]
} else {
  // 查询返回零个结果
}

console.log(cursor.toArray()); // 输出 [{ artistid: 456, artistname: 'Bob' },{ artistid: 789, artistname: 'Charlie' }]
cursor = self.sql.exec("SELECT * FROM artist ORDER BY artistname ASC;")
raw_result = cursor.raw().next()

if not raw_result.done:
  print(raw_result.value)  # 输出 [ 123, 'Alice' ]
else:
  # 查询返回零个结果
  pass

print(cursor.toArray())  # 输出 [{ artistid: 456, artistname: 'Bob' },{ artistid: 789, artistname: 'Charlie' }]

SqlStorageCursor 具有以下属性:

  • columnNames: string[]
    • 按其在 raw 迭代器返回的每行数组中出现的顺序排列的查询列名。
  • rowsRead: number
    • 作为此 SQL query 的一部分,到目前为止读取的行数。随着您迭代游标,该值可能会增加。最终值用于 SQL 计费
  • rowsWritten: number
    • 作为此 SQL query 的一部分,到目前为止写入的行数。随着您迭代游标,该值可能会增加。最终值用于 SQL 计费
  • 列中的任何数值都会受到 JavaScript 的 52 位数字精度的影响。如果您存储一个非常大的数字(在 int64 中),然后检索该值,返回的值可能会比您的原始数字精度更低。

示例 (Examples)

以下 SQL API 示例使用以下 SQL schema:

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

返回的 cursor 行为:

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

返回的 cursor 和 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

databaseSize

databaseSize: number

返回值 (Returns)

当前 SQLite 数据库的大小(以字节为单位)。

let size = ctx.storage.sql.databaseSize;
size = ctx.storage.sql.databaseSize

PITR (时间点恢复) API

对于基于 SQLite 的 Durable Objects,可以使用以下时间点恢复 (PITR) API 方法将 Durable Object 嵌入的 SQLite 数据库恢复到过去 30 天内的任何时间点。这些方法适用于整个 SQLite 数据库内容,包括对象存储的 SQL 数据和使用键值 put() API 存储的键值数据。本地开发不支持 PITR API,因为本地不会存储数据更改的持久日志。

PITR API 使用"书签" (bookmark) 来表示时间点。书签是一个主要由字母数字组成的字符串,例如 0000007b-0000b26e-00001538-0c3e87bb37b3db5cc52eedb93cd3b96b。书签被设计为在词法上是可比较的:使用常规字符串比较,表示较早时间点的书签比表示较晚时间点的书签小。

getCurrentBookmark

ctx.storage.getCurrentBookmark(): Promise<string>

  • 返回一个表示该对象历史记录中当前时间点的书签。

getBookmarkForTime

ctx.storage.getBookmarkForTime(timestamp: number | Date): Promise<string>

  • 返回一个表示大约给定时间点的书签,该时间点必须在过去 30 天内。如果时间戳表示为数字,则会将其转换为日期,就像使用 new Date(timestamp) 一样。

onNextSessionRestoreBookmark

ctx.storage.onNextSessionRestoreBookmark(bookmark: string): Promise<string>

  • 配置 Durable Object,以便在其下一次重启时,将其存储恢复为与给定书签处存储包含的内容完全匹配。调用此方法后,应用程序通常应调用 ctx.abort() 重启 Durable Object,从而完成时间点恢复。

该方法返回一个特殊的书签,表示恢复发生之前的紧邻时间点(即使在技术上该时间点仍处于未来)。因此,在恢复完成后,可以通过对此书签执行第二次恢复来撤销它。

const DAY_MS = 24*60*60*1000;
// 恢复到 2 天前
let bookmark = ctx.storage.getBookmarkForTime(Date.now() - 2 * DAYS_MS);
ctx.storage.onNextSessionRestoreBookmark(bookmark);
from datetime import datetime, timedelta

now = datetime.now()
# 恢复到 2 天前
bookmark = ctx.storage.getBookmarkForTime(now - timedelta(days=2))
ctx.storage.onNextSessionRestoreBookmark(bookmark)

同步 KV API (Synchronous KV API)

get

  • ctx.storage.kv.get(key string): Any, undefined
    • 获取与给定键关联的值。返回值的类型为之前为该键写入的类型;如果键不存在,则返回 undefined。

put

delete

  • ctx.storage.kv.delete(key string): boolean
    • 删除键及其关联的值。如果键存在则返回 true,不存在则返回 false

list

  • ctx.storage.kv.list(options Objectoptional): Iterable<string, any>
    • 返回与当前 Durable Object 关联的所有键和值,按键的 UTF-8 编码升序排序。

    • Iterable 中每个返回值的类型为之前为对应键写入的类型。

    • 在调用不带 options 的 list 版本之前,请注意 Durable Object 中可能存储的数据量,因为所有数据都将加载到 Durable Object 的内存中,可能会达到其限制。如果对此有顾虑,请按下方文档向 list 传递 options。

支持的选项

  • start string

    • 列表结果应从此键开始(包含该键)。
  • startAfter string

    • 列表结果应在此键之后开始(不包含该键)。不能与 start 同时使用。
  • end string

    • 列表结果应在此键结束(不包含该键)。
  • prefix string

    • 将结果限制为仅包含键以该前缀开头的键值对。
  • reverse boolean

    • 如果为 true,则按降序而非默认升序返回结果。
    • 启用 reverse 不会改变 startstartKeyendKey 的含义。start 仍然定义可按字典序返回的最小键(包含),在降序列表中实际作为终点。end 仍然定义列表应考虑的最大键(不包含),在降序列表中实际作为起点。
  • limit number

    • 返回的键值对的最大数量。

异步 KV API (Asynchronous KV API)

get

  • ctx.storage.get(key string, options Object optional): Promise<any>

    • 检索与给定键关联的值。返回值的类型将是先前为该键写入的任何类型,如果键不存在则为 undefined。
  • ctx.storage.get(keys Array<string>, options Objectoptional): Promise<Map<string, any>>

    • 检索与每个提供的键关联的值。Map 中每个返回值的类型将是先前为对应键写入的任何类型。Map 中的结果按 UTF-8 编码升序排序,任何不存在的请求键将被省略。一次最多支持 128 个键。

支持的 options

  • allowConcurrency: boolean

    • 默认情况下,系统在存储操作进行时暂停向 Object 传递 I/O 事件,以避免意外的竞态条件。传递 allowConcurrency: true 以选择退出此行为并允许传递并发事件。
  • noCache: boolean

    • 如果为 true,则键/值不会插入内存缓存。如果键已在缓存中,将返回缓存值,但其 last-used 时间不会更新。当你预期此键在近期不会再次使用时使用。此标志仅为提示。此标志永远不会改变代码的语义,但可能影响性能。

put

delete

  • delete(key string, options Objectoptional): Promise<boolean>

    • 删除键及关联值。如果键存在则返回 true,否则返回 false
  • delete(keys Array<string>, options Objectoptional): Promise<number>

    • 删除提供的键及其关联值。一次最多支持 128 个键。返回删除的键值对数量。

支持的 options

  • put()delete()deleteAll() 支持以下 options:

  • allowUnconfirmed boolean

    • 默认情况下,系统将暂停 Durable Object 的传出网络消息,直到所有先前的写入都已确认刷新到磁盘。如果写入失败,系统将重置 Object、丢弃所有传出消息,并向任何客户端返回错误。

    • 这样,Durable Objects 可以与写入操作并行继续执行,而无需担心过早确认写入,因为除非写入实际成功,否则任何外部方都无法观察 Object 的操作。

    • 任何写入后,后续网络消息可能会略有延迟。某些应用可能认为基于未确认写入进行通信是可以接受的。某些程序可能希望立即允许网络流量。在这种情况下,将 allowUnconfirmed 设置为 true 以选择退出默认行为。

    • 如果你希望某些传出网络消息立即继续但不希望其他消息继续,可以使用 allowUnconfirmed 选项避免阻塞你希望继续的消息,然后单独调用 sync() 方法,该方法返回的 promise 仅在所有先前的写入成功持久化到磁盘后才 resolve。

  • noCache boolean

    • 如果为 true,则键/值在完成写入磁盘后立即从内存中丢弃。

    • 如果键在近期不会再次使用,请使用 noCachenoCache 永远不会改变代码的语义,但可能影响性能。

    • 如果你在写入完成之前使用 get() 检索键,将返回写入缓冲区中的副本,从而确保与最新 put() 调用的一致性。

list

  • list(options Objectoptional): Promise<Map<string, any>>
    • 返回与当前 Durable Object 关联的所有键和值,按键的 UTF-8 编码升序排序。

    • Map 中每个返回值的类型将是先前为对应键写入的任何类型。

    • 在调用不带 options 的 list 版本之前,请注意 Durable Object 中可能存储的数据量,因为所有数据都将加载到 Durable Object 的内存中,可能达到其限制。如果这是顾虑,请按以下文档向 list 传递 options。

支持的 options

  • start string

    • list 结果应开始的键(含)。
  • startAfter string

    • list 结果应在其后开始的第一键(不含)。不能与 start 同时使用。
  • end string

    • list 结果应结束的键(不含)。
  • prefix string

    • 将结果限制为仅包含键以该前缀开头的键值对。
  • reverse boolean

    • 如果为 true,以降序而非默认升序返回结果。
    • 启用 reverse 不会改变 startstartKeyendKey 的含义。start 仍定义可按字典序返回的最小键(含), effectively 作为逆序 list 的端点。end 仍定义 list 应考虑的最大键(不含), effectively 作为逆序 list 的起点。
  • limit number

    • 返回的最大键值对数量。
  • allowConcurrency boolean

    • 与上述 get() 的 option 相同。
  • noCache boolean

    • 与上述 get() 的 option 相同。

闹钟 (Alarms)

getAlarm

  • getAlarm(options Objectoptional): Promise<Number | null>
    • 获取当前 alarm 时间(如果已设置),以自 epoch 起的整数毫秒表示。如果 alarm 尚未开始,或已失败且任何重试尚未开始,则视为已设置 alarm。如果未设置 alarm,getAlarm() 返回 null

支持的选项

  • get() 相同的选项,但不包含 noCache

setAlarm

  • setAlarm(scheduledTime Date | number, options Objectoptional): Promise

    • 设置当前 alarm 时间,接受 JavaScript Date 或自 epoch 起的整数毫秒。

    如果使用等于或早于 Date.now() 的时间调用 setAlarm(),alarm 将被安排在近期异步执行。如果此时 alarm 处理程序正在执行,它不会被取消。Alarm 可精确到毫秒级,通常会在设定时间后几毫秒内执行,但由于维护或故障转移期间的故障,可能会延迟最多一分钟。

deleteAlarm

  • deleteAlarm(options Objectoptional): Promise
    • 如果存在 alarm 则删除。如果 alarm 处理程序当前正在执行,不会取消该处理程序。

支持的选项

  • setAlarm()deleteAlarm() 支持与 put() 相同的选项,但不包含 noCache

其他 (Other)

deleteAll

  • deleteAll(options Objectoptional): Promise
    • 删除所有存储的数据,实际释放 Durable Object 使用的所有存储。对于键值存储后端的 Durable Objects,deleteAll() 删除单个 Durable Object 的所有键和关联值。对于 SQLite 存储后端 的 Durable Objects,deleteAll() 删除 Durable Object 私有 SQLite 数据库的全部内容,包括 SQL 数据和键值数据。
    • 对于键值存储后端的 Durable Objects,进行中的 deleteAll() 操作可能失败,可能留下部分未删除的数据。SQLite 存储后端的 Durable Objects 没有部分 deleteAll() 问题,因为 deleteAll() 操作是原子的(全有或全无)。
    • 对于兼容日期为 2026-02-24 或更晚的 Workers,deleteAll() 还会删除任何活动的 alarm。对于较早的兼容日期,deleteAll() 不会删除 alarm。请单独使用 deleteAlarm(),或启用 delete_all_deletes_alarm 兼容性标志

transactionSync

  • transactionSync(callback): any
    • 仅在使用 SQLite 支持的 Durable Objects 时可用。

    • 在事务中包装 callback() 并调用,返回其结果。

    • 如果 callback() 抛出异常,事务将回滚。

    • 回调必须同步完成,即不应声明为 async 或以其他方式返回 Promise。只有同步存储操作可以是事务的一部分。这旨在与使用 ctx.storage.sql.exec() 的 SQL 查询一起使用,这些查询同步完成。

transaction

  • transaction(closureFunction(txn)): Promise

    • 在单个事务中运行 txn 上调用的存储操作序列,该事务要么成功提交要么中止。

    • 显式事务不再必要。在没有中间 await 的情况下调用的任何一系列写入操作将自动原子提交,系统在 await 读取操作时将阻止并发事件执行(除非你使用 allowConcurrency: true)。因此,一系列读取后接一系列写入(中间没有其他 I/O)自动具有原子性,行为类似事务。

  • txn

    • 提供对上面记录的 put()get()delete()list() 方法的访问,以在当前事务上下文中运行。要在事务闭包中获得事务行为,必须在 txn Object 上调用方法,而不是在顶层 ctx.storage Object 上。

      还支持 rollback() 函数,确保事务期间所做的任何更改将被回滚而非提交。调用 rollback() 后,txn Object 上的任何后续操作都将失败并抛出异常。rollback() 不接受参数且不向调用方返回任何内容。

    • 使用 SQLite 存储引擎 时,txn 对象已过时。直接在 ctx.storage Object 上执行的任何存储操作,包括使用 ctx.storage.sql.exec() 的 SQL 查询,都将被视为事务的一部分。

sync

  • sync(): Promise
    • 将任何待处理的写入同步到磁盘。

    • 这类似于自动写入合并的正常行为。如果写入缓冲区中有任何待处理的写入(包括通过 allowUnconfirmed 选项 提交的写入),返回的 promise 将在它们完成时 resolve。如果没有待处理的写入,返回的 promise 将已 resolve。

存储属性 (Storage properties)

sql

sqlDurableObjectStorage 类型的只读属性,封装了 SQL API

相关资源

这篇文档对您有帮助吗?