Persistence — 持久化与查询
theseed 持久化层:实体存储、查询扩展、Schema 管理。
来源源头:BigWorld
Archiver / SecondaryDB / consolidate_dbs。 参考实现:KBEngine 的 EntityTable CRUD 主路径。 当前实现基线以 ../0-foundation/01-mvp-architecture-baseline 为准。 本篇只讨论主持久化与查询,不展开本地归档暂存层;后者见 03-local-archive-and-secondary-db。
0.5 引擎实现对照与取舍
BigWorld 是怎么实现的
BigWorld 的持久化不是单表 CRUD:
- Archiver 负责归档
- SecondaryDB 负责本地暂存和后续归并
- 还有合服 / 同步 / 修复工具链KBEngine 是怎么实现的
KBEngine 主要是 EntityTable 级的加载与保存,
整体更偏向基础 CRUD,而不是完整数据运维体系。优缺点
BigWorld 的优点:
- 数据层次完整
- 能支撑更复杂的运营动作
KBEngine 的优点:
- 简单
- 容易落地
共同缺点:
- 一旦引入归档、同步、修复,边界必须很清楚theseed 的取舍
theseed 把主持久化和本地归档暂存层拆开,
避免把 SecondaryDB 误写成主库,
也避免把工具链能力塞进在线读写接口。0. MVP 边界
MVP 先解决:
- Entity load / save / remove
- 基础结构化查询
- MySQL 后端
- JSON 列查询
MVP 不要求一次解决:
- 所有后端的统一高级查询语义
- 聚合与原始 SQL 的跨后端等价抽象
- Schema 迁移、合服、查询平台能力全部塞进一个接口
- BaseApp 本地 SecondaryDB / LocalArchiveStore1. JSON 原生支持
1.1 为什么不用 BLOB 存 ARRAY/FIXED_DICT
BLOB 的问题:
1. 无法在 SQL 中查询
2. 无法建索引
3. 无法做统计/聚合
4. 调试困难(SELECT * 看到乱码)
5. 跨语言访问困难
JSON 的优势:
1. 可查询:JSON_EXTRACT(equipment, '$.weapon') = 500
2. 可索引:MySQL 8.0 多值索引、PostgreSQL GIN 索引
3. 可读
4. 可统计
5. 工具友好1.2 JSON 查询接口
python
import theseed
# 结构化查询
results = theseed.db.query("Player") \
.filter(theseed.db.json_path("equipment.weapon") == 500) \
.filter(theseed.db.field("level") >= 50) \
.limit(100) \
.execute()
# 自定义 SQL(仅 MySQL/PG 适用)
results = theseed.db.execute("""
SELECT id, name, JSON_EXTRACT(equipment, '$.weapon') as weapon_id
FROM tbl_Player
WHERE level > :min_level
ORDER BY level DESC
LIMIT :limit
""", min_level=50, limit=100)2. 最小职责接口
核心原则:运行时只依赖最小接口,不让
IStorageBackend变成“数据库平台总入口”。
2.1 Entity 存储接口
cpp
// storage/IEntityStore.h
class IEntityStore {
public:
virtual ~IEntityStore() = default;
virtual Future<EntityData> load(EntityId id, const EntityDef& def) = 0;
virtual Future<void> save(EntityId id,
const EntityData& data,
const EntityDef& def) = 0;
virtual Future<void> remove(EntityId id) = 0;
};2.2 结构化查询接口
cpp
// storage/IEntityQueryStore.h
class IEntityQueryStore {
public:
virtual ~IEntityQueryStore() = default;
virtual Future<std::vector<EntityId>> query(const StorageQuery& q) = 0;
virtual Future<std::vector<EntityId>> queryJsonPath(
const std::string& entityType,
const std::string& jsonPath,
const std::string& op,
const QueryParam& value) = 0;
};2.3 后端特定查询接口
cpp
// storage/IRawQueryExecutor.h
class IRawQueryExecutor {
public:
virtual ~IRawQueryExecutor() = default;
virtual Future<QueryResult> executeRaw(const std::string& sql,
const QueryParams& params) = 0;
virtual Future<QueryResult> executeNamed(const std::string& queryName,
const QueryParams& params) = 0;
};2.4 Schema 管理接口
cpp
// storage/ISchemaMigrator.h
class ISchemaMigrator {
public:
virtual ~ISchemaMigrator() = default;
virtual Future<void> createTable(const EntityDef& def) = 0;
virtual Future<MigrationPlan> planMigration(const EntityDef& oldDef,
const EntityDef& newDef) = 0;
virtual Future<void> executeMigration(const MigrationPlan& plan) = 0;
};2.5 合服接口(非 MVP 主路径)
cpp
// storage/IMergeBackend.h
class IMergeBackend {
public:
virtual ~IMergeBackend() = default;
virtual Future<MergeReport> mergeFrom(const IMergeBackend& source,
const MergeConfig& config) = 0;
};2.6 本地归档暂存接口(非 Runtime Core 主依赖)
cpp
// storage/ILocalArchiveStore.h
class ILocalArchiveStore {
public:
virtual ~ILocalArchiveStore() = default;
virtual Future<void> append(const ArchiveSnapshot& snapshot) = 0;
virtual Future<void> flush() = 0;
virtual Future<ArchiveGenerationMeta> rotate() = 0;
};说明:
ILocalArchiveStore 属于:
- Archiver 增强
- 数据运维工具链
ILocalArchiveStore 不属于:
- Runtime Core 的主持久化依赖
- IEntityStore 的职责范围3. 为什么要拆接口
如果把 CRUD、原始 SQL、命名查询、JSON Path、聚合、Schema 迁移、合服、
本地归档暂存、consolidate 全部塞进一个 IStorageBackend,会出现三个问题:
1. SRP 被破坏
- 一个接口承载太多职责
- Runtime、工具链、运维平台都被迫依赖同一个“大接口”
2. ISP 被破坏
- 内核只想 load/save,却被迫知道 mergeFrom / executeRaw / aggregate
3. 最小公分母设计
- 为了兼容所有后端,抽象会越来越虚
- 最后不是 everywhere if(capabilities),就是一堆不可验证的承诺拆分后的依赖关系:
Runtime Core
→ IEntityStore
业务查询层
→ IEntityQueryStore
运维/数据工具
→ IRawQueryExecutor / ISchemaMigrator / IMergeBackend
归档增强
→ ILocalArchiveStore / IArchiveConsolidator4. MVP 后端策略
4.1 MySQL 后端(首选)
cpp
class MySQLEntityStore : public IEntityStore,
public IEntityQueryStore,
public IRawQueryExecutor,
public ISchemaMigrator {
// MVP 的默认组合
};MySQL 作为 MVP 默认后端的原因:
- 和现有 BigWorld / KBEngine 用户习惯接近
- JSON 支持已经足够实用
- 运维成本低
- 适合先跑通实体生命周期闭环4.2 PostgreSQL / MongoDB(Phase 2)
cpp
class PostgreSQLEntityStore : public IEntityStore,
public IEntityQueryStore,
public IRawQueryExecutor,
public ISchemaMigrator {
// Phase 2
};
class MongoEntityStore : public IEntityStore,
public IEntityQueryStore {
// Phase 2
};说明:
- PG 和 MongoDB 仍然是合理方向
- 但不应该阻塞 MySQL MVP
- 也不应该要求三者首版就具备完全等价的高级能力5. 能力分层
Level 1: 核心运行时必须依赖
- load
- save
- remove
Level 2: 业务开发高频使用
- query
- queryJsonPath
Level 3: 平台和运维增强
- executeRaw
- executeNamed
- schema migration
- merge
- local archive store
- consolidate / transfer / sync设计要求:
- Level 1 必须稳定、简单、可测
- Level 2 可以按后端逐步增强
- Level 3 不得反向污染 Runtime Core6. 开发时校验
6.1 XSD 结构校验(第一层)
XSD 自动校验(编辑器保存时):
✅ 元素结构:Properties 必须包含 property 子元素
✅ 类型枚举:type 只能是 UINT8/UINT32/STRING/FIXED_DICT 等
✅ 必填检查:name 和 type 是 required 属性
✅ 值域约束:size 正整数,persistent 布尔值
✅ Flags 枚举:ALL_CLIENTS | BASE | OWN_CLIENT 等
✅ column_type 枚举:VARCHAR | JSON | SUBTABLE 等6.2 defcheck 语义校验(第二层)
典型校验规则:
- string_without_size (error): STRING 没有 size 限制
- string_size_exceeds_column (error): STRING size 超过列长度
- blob_without_max (warning): BLOB 没有 max 限制
- array_too_large_for_blob (warning): ARRAY 可能超 BLOB 限制
- json_without_path_index (warning): JSON 列建议添加索引
- type_change_breaks_storage (error): 类型变更需要数据迁移6.3 校验流程
def 变更时自动执行:
1. XSD 校验 → 结构错误标红
2. defcheck → 语义校验报告
3. XInclude 合并 → 冲突检测
4. 生成 IDE 提示(.pyi / .d.ts)
5. Schema diff → 迁移计划
6. CI 集成 → error 阻断提交