SpacetimeDB 默认值(Default Values)完全指南:让 Schema 演进不再丢失数据
2026/9/12 21:27:12 网站建设 项目流程

SpacetimeDB 默认值(Default Values)完全指南:让 Schema 演进不再丢失数据

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

导读

默认值(Default Values)是 SpacetimeDB 实现自动迁移(Automatic Migrations)的关键机制:当你在已发布数据库的表末尾新增一个带默认值的列,并重新spacetime publish模块后,数据库中已存在的每一行都会自动被填充为该默认值,无需手工写迁移脚本。本文以官方核心概念文档 Default Values 为骨架,结合仓库内 Rust/TypeScript/C#/C++ 四种 SDK 的源码实现,系统讲解默认值的定义方式、限制条件、常见误区与实际应用场景,读完你将能安全地完成"加列不加锁、数据零丢失"的在线 Schema 演进。

注意:带默认值的新列必须添加在表定义的【末尾】,在表中间插入新列是不被支持的(会在发布时直接失败)。

一、默认值在自动迁移中的角色

在 SpacetimeDB 中,当你对已有数据库执行spacetime publish {database-name}时,系统会尝试把现有数据库 Schema 自动迁移到新模块定义的形态。所谓 Schema,指模块代码中声明的表、reducer、procedure、view 及其依赖类型的集合。

根据 Automatic Migrations 文档,默认值恰好处于"可能有破坏性(Potentially Breaking)"与"禁止(Forbidden)"两类变更的边界上:

  • 允许:向表末尾添加带默认值的新列——已有行会被自动填充,但未更新版本的客户端将感知不到新列;
  • 禁止:添加不带默认值的新列——因为已有行无法被填充,发布会失败;
  • 禁止:在表中间添加新列。

因此,默认值不是可选项,而是"向已存在数据的表添加新列"这一操作能否成功的硬性前提。它让"为玩家表新增score字段""为订单表新增status字段"这类高频演进需求变成一次普通的重新发布。

二、在四种服务端语言中定义默认值

默认值可以链式作用于任何列类型构建器 / 属性 / 宏上,但要求值必须与列类型严格匹配(类型不匹配在编译期即报错)。以下四个示例分别来自 Default Values 原文档,四段代码等价。

TypeScript(.default(value)

const player = table( { name: 'player', public: true }, { id: t.u64().primaryKey().autoInc(), name: t.string(), // 新添加的带默认值的列 score: t.u32().default(0), isActive: t.bool().default(true), bio: t.string().default(''), } );

.default(value)方法可以链式调用在任意列类型构建器上。从源码看,type_builders.ts 中每种列构建器(U32ColumnBuilderBoolColumnBuilderStringColumnBuilderU64ColumnBuilderUuidColumnBuilderTimestampColumnBuilder等 20 余种)都实现了default(),其本质是向列的元数据中写入{ defaultValue: value },返回值类型也随之收紧为SetField<M, 'defaultValue', Type>,从而在类型系统层面保证默认值与列类型一致。

值得注意的细节:

  • 整数类型(如t.u64())的默认值需要用bigint字面量(如0n),而小整数类型(t.u8()t.u32())直接用number
  • t.identity()t.connectionId()t.timestamp()t.scheduleAt()等特殊类型同样支持默认值。

C#([SpacetimeDB.Default(value)]

[SpacetimeDB.Table(Accessor = "Player", Public = true)] public partial struct Player { [SpacetimeDB.PrimaryKey] [SpacetimeDB.AutoInc] public ulong Id; public string Name; // 新添加的带默认值的列 [SpacetimeDB.Default(0u)] public uint Score; [SpacetimeDB.Default(true)] public bool IsActive; [SpacetimeDB.Default("")] public string Bio; }

[SpacetimeDB.Default(value)]特性声明默认值,值会按列类型被序列化。其底层实现在 Runtime/Attrs.cs 中:

/// Specifies a default value for a table column. /// If a column is added to an existing table while republishing of a module, /// the specified default value will be used to populate existing rows. [AttributeUsage(AttributeTargets.Field)] public sealed class DefaultAttribute(object value) : Internal.ColumnAttribute

该特性只能标注在**字段(Field)**上,构造函数接收原始值对象,其Value属性会根据值类型做字符串化处理——例如null会输出为"null"bool、数值、字符串各有对应处理分支。

Rust(#[default(value)]

#[spacetimedb::table(accessor = player, public)] pub struct Player { #[primary_key] #[auto_inc] id: u64, name: String, // 新添加的带默认值的列 #[default(0)] score: u32, #[default(true)] is_active: bool, #[default("")] bio: String, }

#[default(value)]属性声明默认值,表达式必须可 const 求值(能在const上下文中使用),因此传入的必须是字面量或常量表达式,不能是运行时计算值。

结合 bindings-macro/src/table.rs 的宏实现,可以看清 Rust 侧默认值的完整处理链路:

  1. 属性解析ColumnAttr::parse识别default属性,parse_default_attr通过attr.parse_args::<syn::Expr>()提取表达式(table.rs);
  2. 合法性校验:如果某列同时带有defaultauto_inc/primary_key/unique,宏直接编译失败,报错信息为"invalid combination: auto_inc, unique index or primary key cannot have a default value"(table.rs);
  3. 编译期类型检查:宏会为每个带默认值的列生成let _check: #ty = #val;(字符串列则生成let _check: &'static str = #val;),把类型不匹配提前到编译期拦截(table.rs);
  4. 运行时序列化:宏生成get_default_col_values()方法,返回Vec<spacetimedb::table::ColumnDefault>,其中每个默认值通过sats::algebraic_value::ser::ValueSerializer序列化为代数值(Algebraic Value),在发布时随 Schema 一起提交给数据库(table.rs)。

也就是说,你写的#[default(0)]最终会成为数据库迁移时针对col_id所对应列的填充指令。

C++(FIELD_Default宏)

struct Player { uint64_t id; std::string name; uint32_t score; bool is_active; std::string bio; }; SPACETIMEDB_STRUCT(Player, id, name, score, is_active, bio) SPACETIMEDB_TABLE(Player, player, Public) FIELD_PrimaryKeyAutoInc(player, id) FIELD_Default(player, score, 0u) FIELD_Default(player, is_active, true) FIELD_Default(player, bio, std::string(""))

C++ 端在表注册之后,通过FIELD_Default(table, field, value)宏声明各列的默认值。这些默认值会在新增列触发的 Schema 迁移期间被应用。

注意:C++ 模块能力随版本演进,相关能力请以CppModuleVersionNotice提示的版本要求为准。

三、限制条件:默认值不能与哪些属性共存

默认值不能与以下属性组合使用:

  • 主键(Primary keys)
  • 唯一约束(Unique constraints)
  • 自增列(Auto-increment)

这一限制的根本原因在于:主键、唯一约束、自增都由数据库代为管理列值,这与"提供一个静态默认值"的语义直接冲突。两种机制都要决定"这个列的值从哪来",不能同时生效。

在 Rust 侧,这一限制由过程宏在编译期强制(见上文 table.rs 的报错);自增列文档 Auto-Increment 的结尾也明确写到:"Auto-incrementcannotbe combined with default values, since both attempt to populate the column automatically."

四、TypeScript 的"非直觉错误信息"排障指南

在 TypeScript 中,上述限制在编译期强制,但报错信息并不直观。如果你违反了规则,你会看到:

Expected 3 arguments, but got 2.

这个报错意味着你的某一列出现了.default().primaryKey().unique().autoInc()的非法组合。

例如,下面的代码就会触发该错误:

// ERROR: default() + primaryKey() is not allowed const badTable = table( { name: 'bad' }, { id: t.u64().default(0n).primaryKey() } // <- 触发 "Expected 3 arguments" );

修复方法:移除.default()或移除约束(.primaryKey()/.unique()/.autoInc()),二者留其一即可。

这条报错的成因与 SDK 的类型设计有关:合法的列构建器会因元数据类型的不同而拥有不同的重载签名,当default()与主键/唯一/自增元数据发生冲突时,类型推断会让table()的调用落回到一个"期望 3 个参数"的不匹配重载上,从而产生这条令人困惑的信息。排查时可逐个检查新加的列,凡是用到default()的列都不要附加上述约束。

五、典型应用场景

  • Schema 演进:为应用新增功能而不丢失既有数据。例如游戏上线后为player表新增score列,老玩家数据自动得到score = 0
  • 可选字段:为历史上未被记录的字段提供合理默认值,例如bio: string().default(''),避免出现空值判断逻辑;
  • 功能开关(Feature Flags):新增default(false)的布尔列,通过逐步把某行置为true来灰度上线新功能,这正是 Automatic Migrations 文档推荐的最佳实践之一。

六、与"自增列"划清边界

默认值与自增列是两个互斥的自动填充机制,理解两者的分工有助于设计正确的表结构:

  • 默认值:在"新增列 + 自动迁移"场景下为已有行填充静态值;
  • 自增列(autoInc()/#[auto_inc]/[SpacetimeDB.AutoInc]:在插入新行时由内部序列(sequence)生成递增整数,适用于主键/编号字段。

一个常见且正确的组合是"主键 + 自增":

#[spacetimedb::table(accessor = post, public)] pub struct Post { #[primary_key] #[auto_inc] id: u64, // ... }

而"默认值 + 主键 / 唯一 / 自增"则是非法组合。在设计表时,请先明确某列的值由"数据库生成"还是"静态指定",再决定使用哪种机制。

七、生产环境使用建议

结合 Automatic Migrations 文档的迁移策略,使用默认值进行 Schema 演进时有几点实操建议:

  1. 新列一律加到表尾:无论是哪种语言,带默认值的新列都必须追加在表定义末尾,不要在中间插入,否则发布失败;
  2. 先测试再上线:在包含样例数据的开发库上演练一次迁移,确认已有行的默认值填充符合预期,再发布到生产;
  3. 注意客户端兼容性:自动迁移会保持活跃客户端连接,但未更新版本的客户端不会感知新列,涉及新列的逻辑需同步更新并重新生成客户端 bindings;
  4. 开发期可--delete-dataspacetime publish --delete-data会清空数据重建库,仅限开发测试使用,严禁用于生产
  5. 复杂的结构性变更走增量迁移:若需要删除非空表、修改列类型/顺序等自动迁移不支持的操作,请参考 Incremental Migrations 的生产级模式。

小结

默认值是 SpacetimeDB 无停机 Schema 演进的基石:只要遵循"新列加在表尾 + 必须带默认值 + 不与主键/唯一/自增共存"三条规则,就能通过一次spacetime publish平滑地为已有数据补充新字段。在 Rust 中,#[default(...)]宏提供了编译期类型检查与序列化的完整保障;在 TypeScript 中要留意Expected 3 arguments这一非直觉报错的真实含义;在 C# / C++ 中则分别通过[SpacetimeDB.Default]特性与FIELD_Default宏声明。结合本文给出的源码路径,你可以在 bindings-macro/src/table.rs、type_builders.ts 与 Runtime/Attrs.cs 中进一步验证这些机制的具体实现。

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询