Schema 演进
Schema 演进允许应用的不同版本安全交换消息:v2 写入端可以生成 v1 读取端仍能解码的消息,反之亦然。
兼容模式
兼容模式是 Dart 的默认设置。当服务可能同时运行不同版本时,请保留此默认值,例如滚动部署期间或客户端未立即更新时。
final fory = Fory();
在兼容模式下,Fory 会在每条消息中包含足够的字段元数据,使读取端可以跳过未知字段,并对缺失字段使用默认值。使用稳定字段 ID(见下文)在变更期间固定 Schema。
当值可以无损转换时,兼容读取端还允许部分标量字段类型变化。只要转换后的逻辑值相同,匹配字段就可以在 bool、String、数值标量和 Decimal 之间读取。例如,"true" 和 "false" 可以读取为布尔值;"123" 可以读取为能够容纳 123 的数值字段;数值和 decimal 可以读取为规范字符串;数值拓宽或收窄仅在不损失精度或范围时成功。标量转换仅适用于匹配的兼容字段,不适用于根值或集合元素。字符串转数值只接受有限 ASCII 十进制字面量,不允许空白、前导 +、Unicode 数字、下划线、NaN 或 Infinity。可空字段仍可与这些转换组合,但启用引用跟踪的标量类型变化不兼容。无效字符串、超出范围的值和有损转换会在反序列化期间以 InvalidDataException 失败。
配置 Schema 演进
为了安全使用兼容模式,请用 @ForyStruct(evolving: true)(默认设置)标记 struct,并在发布首个载荷前为每个字段分配稳定的 @ForyField(id: ...):
(evolving: true)
class UserProfile {
UserProfile();
(id: 1)
String name = '';
(id: 2, nullable: true)
String? nickname;
}
如果在载荷投入生产后才添加字段 ID,已有存储消息不会包含这些 ID,Schema 演进将无法正常工作。
对于普通继承 struct,请为具体子类扁平 Schema 纳入的每个字段分配 ID。子类不能复用已由纳入的父类或 mixin 字段使用的 ID。
对于外部结构化序列化器,本地序列化器声明提供演进 Schema,每个声明字段必须与对应目标属性匹配。
可以安全进行的变更
安全变更(双方都兼容):
- 使用新的未占用字段 ID 添加可选字段。
- 重命名字段,但保持
@ForyField(id: ...)不变。 - 删除字段;通信方会忽略缺失值并使用 Dart 默认值。
- 当所有已部署值转换后不会损失精度或范围时,更改部分标量字段类型。
不安全变更(可能破坏现有消息):
- 为其他字段复用现有字段 ID。
- 将字段类型改为不兼容类型,或无法精确表示通信方值的标量类型。
- 消息投入生产后修改类型的注册标识(
id或name)。 - 不修改 ID,却改变字段的逻辑含义。
- 引入字段隐藏,同时保留祖先和子类存储槽;代码生成会拒绝这种结构,因为无法准确寻址两个存储槽。
跨语言说明
只有交换消息的所有通信方就以下内容达成一致时,演进才能工作:
- 相同的
compatible设置。 - 相同的类型注册标识(数字 ID 或
name)。 - 字段 ID 的逻辑含义。
部署前使用真实往返测试滚动升级场景。
相同 Schema 优化
仅当每个读取端和写入端始终使用相同 Schema,并且希望获得更快序列化和更小体积时,才使用 compatible: false。对于 xlang 载荷,只有确认每种语言使用相同 Schema,或原生类型由 Fory Schema IDL 生成时,才设置 compatible: false。
final fory = Fory(compatible: false);
重新生成继承 Schema
新增继承存储或者修改父类或 mixin 后,请重新生成每个受影响的 .fory.dart part;修改 exposePrivateFields 或 ignoreInheritedPrivateFields 后也需要重新生成。构建纳入依赖包私有字段的消费方前,请先生成该依赖包的提供方 part。
启用 ignoreInheritedPrivateFields 会从该具体子类的生成 Schema 中删除每个私有祖先字段和所应用 mixin 字段。禁用它会将这些字段加回,并可能需要提供方配套类型。兼容 Schema 使用普通的缺失字段和未知字段行为。由于生成的字段列表不同,固定 Schema 通信方必须同步变更。父类注解不会将此设置传播给子类。
此选项只改变生成字段选择,不会改变运行时引用协议或添加兼容读取器。