Schema Mapper
Status: deferred extension design.
The current Shield refactor keeps only raw DB / Redis access in core. Schema mapper is not part of the runtime core contract and must be treated as a later optional extension unless the roadmap explicitly changes.
Schema mapper 是 Shield 契约系统中的数据访问定义层。它参考 MyBatis 的显式 mapper 思路,用 XML 描述 SQL、参数绑定、结果映射和事务边界,但不把 Shield 变成重 ORM。
设计目标
- 探索数据库访问的可选扩展形态,不进入当前 runtime core。
- 通过 XML 契约生成服务端 mapper 接口和运行时元数据。
- 显式 SQL 优先,避免隐式查询和对象图级联。
- 共享 schema 类型系统,但不把 RPC DTO 等同于数据库 entity。
- mapper 只进入服务端 descriptor,默认不下发客户端。
- 第一版支持关系型数据库,后续再扩展 Redis、MongoDB 等后端。
边界
mapper 负责:
- SQL statement 定义
- 参数绑定
- 结果映射
- 事务策略
- 分页约束
- 批量操作
- 可选缓存提示
- 生成服务端接口
mapper 不负责:
- 客户端协议
- 服务路由
- 业务流程编排
- 自动对象图加载
- ActiveRecord 生命周期
- 跨服务分布式事务
文件位置
mapper 文件放在业务模块目录下:
protocol/
player/
types.xml
services.xml
mappers.xml一个模块可以拆多个 mapper 文件。manifest 可以支持显式 include:
<module name="player" path="player">
<types file="types.xml"/>
<services file="services.xml"/>
<mappers file="mappers.xml"/>
<mappers file="inventory_mappers.xml"/>
</module>Entity 和 DTO
mapper 可以定义 entity,但 entity 是持久化模型,不等同于 RPC DTO。
<entity name="PlayerEntity" table="player">
<field name="player_id" column="player_id" type="string" id="1" primaryKey="true"/>
<field name="nickname" column="nickname" type="string" id="2"/>
<field name="level" column="level" type="int32" id="3"/>
<field name="created_at" column="created_at" type="int64" id="4"/>
<field name="updated_at" column="updated_at" type="int64" id="5"/>
</entity>RPC 返回结构应该使用 DTO 或 view:
<struct name="PlayerProfile">
<field name="player_id" id="1" type="string"/>
<field name="nickname" id="2" type="string"/>
<field name="level" id="3" type="int32"/>
</struct>这可以避免数据库字段变化直接污染客户端协议。
公共类型、entity/view 边界和校验规则见 Schema Types。
Mapper 定义
<mappers namespace="player">
<mapper name="PlayerMapper" id="100">
<select name="SelectProfile"
id="1"
paramType="player.GetProfileRequest"
resultType="player.PlayerProfile"
timeout_ms="1000">
SELECT player_id, nickname, level
FROM player
WHERE player_id = #{player_id}
</select>
<update name="UpdateNickname"
id="2"
paramType="player.UpdateNicknameRequest"
resultType="common.AffectedRows">
UPDATE player
SET nickname = #{nickname}
WHERE player_id = #{player_id}
</update>
</mapper>
</mappers>规则:
mapper id在模块内唯一。statement id在 mapper 内唯一。- 发布后的 ID 不能复用。
paramType必须引用 schema type。resultType或resultMap必须显式声明。- SQL 中只能使用声明式参数绑定,不允许字符串拼接参数。
Statement 类型
第一版支持:
selectinsertupdatedelete
后续可以扩展:
batchInsertbatchUpdateupsertcallProcedure
参数绑定
参数使用 #{name} 绑定,表示安全 prepared statement 参数。
WHERE player_id = #{player_id}${name} 原样替换不进入 Phase 1。后续如需支持,只能用于经过白名单校验的标识符场景,例如排序字段:
<bind name="order_by" source="order_by" mode="identifier" allow="level,nickname,created_at"/>Phase 1 只实现 #{},不实现 ${}。
嵌套参数
支持点路径:
WHERE guild_id = #{filter.guild_id}
AND level >= #{filter.min_level}路径必须能在 paramType 中静态解析,编译期校验失败则拒绝生成 descriptor。
Result Type
简单结果可以直接用 resultType:
<select name="SelectProfile"
id="1"
paramType="player.GetProfileRequest"
resultType="player.PlayerProfile">
SELECT player_id, nickname, level
FROM player
WHERE player_id = #{player_id}
</select>字段名默认按 column name 匹配 struct field name。Phase 1 允许 snake_case 到 camelCase 映射,但必须通过 mapper 配置显式开启。
Result Map
复杂映射使用 resultMap:
<resultMap name="PlayerProfileMap" type="player.PlayerProfile">
<result column="player_id" property="player_id"/>
<result column="nickname" property="nickname"/>
<result column="level" property="level"/>
</resultMap>
<select name="SelectProfile"
id="1"
paramType="player.GetProfileRequest"
resultMap="PlayerProfileMap">
SELECT p.player_id, p.nickname, p.level
FROM player p
WHERE p.player_id = #{player_id}
</select>第一版 resultMap 不做自动关联加载。需要 join 时,用显式 SQL 返回扁平 DTO。
返回形态
select 默认返回单条。列表查询显式声明:
<select name="ListPlayers"
id="2"
paramType="player.ListPlayersRequest"
resultType="list<player.PlayerProfile>"
maxRows="100">
SELECT player_id, nickname, level
FROM player
WHERE level >= #{min_level}
ORDER BY level DESC
LIMIT #{limit}
</select>规则:
- 列表查询必须有
maxRows。 - 分页查询必须有上限。
- 没有
WHERE的update/delete默认拒绝,除非显式allowFullTable="true"。
事务
第一版支持 statement 级事务提示,但一个 mapper statement 只能包含一条 SQL。多步事务由 service 层显式编排。
<update name="DebitGold"
id="10"
paramType="player.DebitGoldRequest"
resultType="common.AffectedRows"
transaction="required">
UPDATE wallet
SET gold = gold - #{amount}
WHERE player_id = #{player_id}
AND gold >= #{amount}
</update>事务策略:
none: 不主动开启事务。required: 没有事务则开启,有则复用。requires_new: 总是开启新事务。
Phase 1 不做跨 mapper 自动事务编排。跨多步业务事务由 service 层显式控制:
shield.db.transaction(function(tx)
shield.db.PlayerMapper:DebitGold(tx, { player_id = from_id, amount = amount })
shield.db.PlayerMapper:CreditGold(tx, { player_id = to_id, amount = amount })
end)禁止在单个 mapper statement 中编写多条 SQL,避免 prepared statement 行为差异、结果映射复杂化和驱动兼容问题。
动态 SQL
动态 SQL 很实用,但也是复杂度来源。Phase 1 只支持最小集合:
<select name="SearchPlayers"
id="3"
paramType="player.SearchPlayersRequest"
resultType="list<player.PlayerProfile>"
maxRows="100">
SELECT player_id, nickname, level
FROM player
<where>
<if test="nickname != null">
nickname LIKE #{nickname}
</if>
<if test="min_level != null">
AND level >= #{min_level}
</if>
</where>
</select>Phase 1 支持:
ifwheresetforeach用于IN列表
Phase 1 不支持:
- 任意表达式语言
- include fragment
- provider method
- 用户自定义 SQL 节点
表达式语言必须受限,不能执行脚本。
分页
分页应该由契约显式表达:
<select name="ListPlayers"
id="4"
paramType="common.PageRequest"
resultType="common.PageResult<player.PlayerProfile>"
paging="offset"
maxRows="100">
SELECT player_id, nickname, level
FROM player
ORDER BY player_id
LIMIT #{limit} OFFSET #{offset}
</select>cursor paging 推迟到 Phase 2:
<select name="ListPlayersByCursor"
id="5"
paramType="common.CursorRequest"
resultType="common.CursorResult<player.PlayerProfile>"
paging="cursor">
SELECT player_id, nickname, level
FROM player
WHERE player_id > #{cursor}
ORDER BY player_id
LIMIT #{limit}
</select>缓存提示
缓存先作为 hint,不作为强制语义:
<select name="SelectProfile"
id="1"
paramType="player.GetProfileRequest"
resultType="player.PlayerProfile"
cache="local"
cacheKey="player:{player_id}"
ttl_ms="30000">
SELECT player_id, nickname, level
FROM player
WHERE player_id = #{player_id}
</select>缓存策略取值:
nonelocalredis
Phase 1 只生成缓存元数据,不实现复杂一致性策略。写操作后的缓存失效必须显式声明:
<update name="UpdateNickname"
id="2"
paramType="player.UpdateNicknameRequest"
resultType="common.AffectedRows"
invalidateCache="player:{player_id}">
UPDATE player SET nickname = #{nickname}
WHERE player_id = #{player_id}
</update>数据库方言
SQL 方言需要显式声明:
<mappers namespace="player" dialect="mysql">可选值:
mysqlpostgressqlite
Phase 1 支持 mysql 和 sqlite。postgres 保留为 schema 可声明方言,但 runtime 支持推迟到 Phase 2。
同一个 statement 可以提供多个方言版本:
<select name="Now" id="20" resultType="common.Timestamp">
<sql dialect="mysql">SELECT UNIX_TIMESTAMP() AS value</sql>
<sql dialect="sqlite">SELECT strftime('%s','now') AS value</sql>
</select>Generated Server API
生成器应输出服务端 mapper 接口。
C++ 示例:
class PlayerMapper {
public:
task<PlayerProfile> SelectProfile(const GetProfileRequest& req);
task<AffectedRows> UpdateNickname(const UpdateNicknameRequest& req);
};Lua 示例:
local profile = shield.db.PlayerMapper:SelectProfile({ player_id = player_id })生成代码只做类型和调用体验,真正执行仍由 mapper runtime 按 descriptor 处理。
Runtime Flow
service handler
-> generated mapper facade
-> mapper runtime
-> resolve statement descriptor
-> bind parameters
-> prepare statement
-> execute
-> map result rows
-> return typed result or ShieldErrormapper runtime 必须记录:
- mapper name
- statement name
- SQL hash
- duration
- affected rows
- row count
- error code
这些数据进入日志和 metrics。
Descriptor Profile
mapper 默认只进入 server profile:
descriptor.server.bin
types
services
errors
mappers
descriptor.client.bin
types exposed to client
services exposed to client
errors exposed to client客户端不应收到 SQL、表名、列名、缓存 key 或事务策略。
安全规则
编译期必须校验:
- SQL 参数都能在
paramType中解析。 resultType字段能被查询列覆盖,除非字段 optional。update/delete默认必须有WHERE。- 列表查询必须有
maxRows。 - 动态 SQL 表达式不能执行任意脚本。
- 不允许未声明的原样字符串替换。
运行期必须限制:
- statement timeout
- 最大返回行数
- 最大参数数量
- 最大 SQL 长度
- 连接池资源
Phase 1 范围
Phase 1 mapper 只做:
select/insert/update/delete#{}参数绑定resultType- 简单
resultMap - statement timeout
maxRowstransaction=required- server-only descriptor profile
- C++/Lua mapper facade
- 单 statement 单 SQL
推迟:
- 复杂动态 SQL
- 多方言 statement
- 分布式事务
- mapper 多语句脚本
- 自动关联加载
- 二级缓存一致性
- schema migration
已定规则
- Lua mapper facade 不能阻塞 actor 线程,表面同步 API 应基于 coroutine/yield,另提供 async 回调 API。
- SQL 第一版不允许多语句。
- mapper 只能作为后续可选扩展讨论,不进入当前最小启动路径。
- mapper runtime 绑定
shield_data提供的现有 DB 抽象,不新增 lightweight DB runtime。 entity不生成 schema migration 草案;migration 属于独立工具链或应用层责任。