Skip to content

Bean 数据模型

本文是 Zeze Bean 的完整 API 参考——覆盖类型系统、字段 id 规则、托管状态、DynamicBean、Data 类和版本兼容,供写代码时随查随用。XML 声明语法见 solution.xml 参考,概念讲解见 定义数据

Bean 是 Zeze 里结构化数据的基本单元,作用类似 ORM 中的实体(Entity)。它由若干 variable(变量)组成,自动参与事务管理:字段一旦被修改,框架会追踪这次修改、记录事务日志,事务提交时原子地持久化。

Bean 的核心特性:

特性说明
自动事务追踪字段修改自动记日志,提交时原子生效
版本兼容按 variable id 而非字段名序列化,支持加删字段
树形结构variable 和容器构成树,不重复引用、不成环
多态dynamic 类型支持运行时多态

类型Java 对应说明
bytebyte8 位有符号整数
shortshort16 位有符号整数
intint32 位有符号整数
longlong64 位有符号整数
floatfloat32 位浮点
doubledouble64 位浮点
boolboolean布尔
stringStringUTF-8 字符串
binaryZeze.Net.Binary字节序列
decimaljava.math.BigDecimal高精度十进制(不可变类型)
类型说明
vector2两 float(8 字节)
vector2int两 int
vector3三 float
vector3int三 int
vector4四 float
quaternion四元数
类型说明
list有序列表,元素可以是 Bean
set无序集合
map键值映射

容器支持任意嵌套(map 的 value 可以是另一个 Bean,Bean 里又可以有 map)。

类型说明
Bean 名把一个 Bean 作为另一个 Bean 的字段
dynamic运行时多态字段,见下文专门章节

反序列化时框架会在兼容类型之间自动转换,不会因类型微调让历史数据失效:

转换方向说明
数值 ↔ 数值byte/short/int/long 之间
bool ↔ 数值
binarystring
listset

variable id:序列化和版本兼容的真正「键」

Section titled “variable id:序列化和版本兼容的真正「键」”

每个 variable 在 Bean 内部有一个唯一 id(正整数,最大 4095)。这是 Zeze 数据模型里最重要的概念之一。

规则说明
id 用于序列化和版本兼容框架按 id 定位字段,不是按 Java 字段名
id 在 Bean 内唯一同一 Bean 内不能重复
新增分配新 id
删除后不可回收否则旧数据会读到错字段,造成数据错乱
想复用须定义完全一致id、类型、含义全部相同才允许「重新启用」
// 你可以自由重命名字段,只要 id 不变:
// XML: <variable id="1" name="level" type="int"/>
// ↓ 改名,id 不变,数据兼容
// XML: <variable id="1" name="playerLevel" type="int"/>

为什么不可回收? 假设字段 A(id=3)被删了,你又把新字段 B 分配了 id=3。线上的旧数据里 id=3 存的是 A 的值,反序列化时会被读进 B——类型还对的话更危险,悄无声息地错。


托管状态:被框架「接管」的 Bean

Section titled “托管状态:被框架「接管」的 Bean”

Bean 有两种状态,决定了修改是否被事务追踪:

状态说明修改是否记日志
非托管你自己 new 出来的 Bean
托管被放进 Table,或加入已托管容器后
Item item = new Item(); // 非托管,改字段不记日志
item.setCount(5); // 不被追踪
Player player = table.getOrAdd(roleId); // 托管
player.getItems().put(1, item); // item 现在被托管
item.setCount(10); // 这次修改被追踪
规则说明
托管不可逆一旦托管,即使从 Table 移除仍保持托管
重用须 copy()想把已托管 Bean「重置」回可重用状态,调用 copy() 得到非托管副本
检测方法isManaged() 返回是否托管

从 Table 为根出发,Bean 的 variable 和容器构成一棵树

约束说明
不重复引用同一个节点不会被引用两次
不成环不会形成引用环
父子关联parent() 取父节点,variableId() 取所属字段 id

这种树形约束是事务追踪得以简单可靠的基础。


用一个字段在不同时刻持有不同类型的 Bean。用 type="dynamic" 声明,再用 <value> 列出可能类型。

// 取当前实际持有的 Bean
Pet pet = role.getPartner().getBean();
// 取 typeId(标识当前是哪种类型)
int typeId = role.getPartner().getTypeId();
// 设置 Bean(必须是用 <value> 列出过的类型)
role.getPartner().setBean(new Mount());
// 是否未设置
boolean empty = role.getPartner().isEmpty();
方法说明
getBean()取当前持有的 Bean(返回值需自行强转)
setBean(bean)设置 Bean,必须声明过
getTypeId()取当前 typeId,标识持有哪种类型
isEmpty()是否未设置
约束说明
未设置时内部用 EmptyBean(typeId=0)表示
不支持嵌套dynamic 字段里再套 dynamic 不允许

每个 Bean 都对应一个 Data 类,是一份纯数据快照,不参与事务。专门用于跨线程传递和 RPC 序列化。

方法说明
toData()把当前托管 Bean 导出成一份快照
assign(data)用一份快照的数据覆盖当前 Bean
copy()复制一份非托管的 Bean
reset()清空数据(恢复默认值)
// 跨线程传递:先 toData 取快照
PlayerData snapshot = player.toData();
executor.submit(() -> process(snapshot)); // 传不可变快照
// 从快照恢复
player.assign(snapshot);

Data 与 Bean 的集合相互转换:

方法说明
Bean.toDataList(beanList)List → List
Bean.toBeanList(dataList)List → List(新建非托管)
Bean.toDataMap(beanMap)Map<K,Bean> → Map<K,Data>
Bean.toBeanMap(dataMap)Map<K,Data> → Map<K,Bean>(新建非托管)

数据结构会随业务演进,Zeze 的兼容性规则让你在不停服前提下加字段、删字段:

变更处理方式
新增 variable旧数据反序列化时该字段取默认值
删除 variable旧数据里对应的 id 被忽略
类型变更仅限兼容类型间(数值之间、bool↔数值、binary↔string、list↔set)

兼容性只看 variable 的 id 和类型,与类名、typeId 无关。 只要 id 和类型对得上,数据就能正确读出来。


用途说明
开发期重置字段的 id 为连续值,让生成的代码更整洁
发布后绝不能用,会改变既有字段 id,是一次不兼容修改

Bean 可声明一个类型为 longversion 字段(在 XML 里用 <bean name="Player" version="ver">,其中 version 属性的值 ver 是本 Bean 内一个已声明的 long 变量的名字,框架会校验该变量存在且为 long 类型):

规则说明
定义后对 Bean 的修改让 version 自动递增
仅对 Table.Value 有效作为表值的那个 Bean
非 Table.Valueversion() 返回 0
删除重建version 从 0 重新开始
用途乐观锁、变更追踪
Player player = table.getOrAdd(roleId);
long oldVer = player.version(); // 方法名是 version(),不是 getVersion()
player.setLevel(player.getLevel() + 1);
long newVer = player.version(); // 自动 +1

一个典型的 Bean 定义与使用:

// XML:
// <bean name="Player" version="ver">
// <variable id="1" name="name" type="string"/>
// <variable id="2" name="level" type="int"/>
// <variable id="3" name="coins" type="long"/>
// <variable id="4" name="items" type="map[int,Item]"/>
// </bean>
// 使用(在事务内)
Player player = tableTPlayer.getOrAdd(roleId);
player.setLevel(player.getLevel() + 1); // 被追踪
player.getCoins(); // 读取
player.getItems().put(100, new Item(100, 1)); // 嵌套 Bean 托管
// 注:框架生成的 Bean 只有无参构造;这里的带参构造需业务自行实现
// 导出快照(跨线程/RPC)
PlayerData data = player.toData();
// 复制
Player copy = player.copy(); // 非托管副本