NewLife.XCode 是一个有 10 多年历史的开源数据中间件, 支持 nfx/netstandard, 由新生命团队 (2002~2019) 开发完成并维护至今, 以下简称 XCode.
整个系列教程会大量结合示例代码和运行日志来进行深入分析, 蕴含多年开发经验于其中, 代表作有百亿级大数据实时计算项目.
开源地址: https://github.com/NewLifeX/X https://github.com/NewLifeX/X (求 star, 743+)
为何需要扩展属性
XCode 不支持多表关联查询, 单表查询利于优化以及分表分库, 一切 Join 都可以借助扩展属性实现, 配合缓存使用可以达到更好的效果!
(XCode 前期支持多表关联, 直到 2008 年才正式废除)
"扩展属性" 是 2007 年起 XCode 特有叫法, 不同于其它任何场景的意义(如 Silverlight/WPF)
前文《实体类详解》中有提到一个学生班级的实体类模型, 一个典型需求是查询学生列表时希望暂时班级名称或者其它信息. 于是有:
select s.*, c.name where student s left join class c on s.classid=c.id
sql 语法千变万化, 如果要支持多表关联 join, 就很难做到统一查询风格, 更是难以优化.
于是 XCode 放弃支持多表关联, 宁可拆分为多次查询. 令人惊讶的是, 不仅性能没有下降, 反而大大提升了, 主要因为单表小查询有多级缓存的加持!
扩展属性用法
使用扩展属性来实现关联查询, 本质上就是多次查询!
如上, 这是一个经典的多表关联场景, 学生表带有班级 ID 字段, 同样还有产品和分类表等等.
这是 XCode 根据模型文件自动生成的代码, 因为字段名 ClassID 刚好是 Class 表加上它的主键 ID, 并且都是整型.
对于实体对象来说, student.Name 是学生名称, student.ClassName 是班级名称.
看起来它们就像是一张表的属性字段, 这就是扩展属性的由来, 不仅仅是多表关联属性, 还可以是其它属性, 为区别于数据字段属性, 统称为扩展属性!
扩展属性先准备一个 Class 属性, 再加一个 ClassName, 主要是为了方便某些场合使用 student.Class.
当然, 执行一次查询得到 student 后, 不敢是访问 student.Class 还是访问 student.ClassName, 都会触发一次 Class.FindByID, 可以理解为执行一次查询(不一定是数据库).
在 web 页面上, 如果每页显示 20 个学生, 那么先要执行 select * from student limit 20, 然后展示学生列表时, 因为需要班级名称, 触发扩展属性查询.
可以认为, 理论上这个页面需要查询 1+20 次.
扩展属性为什么不写成 public Class Class => Class.FindByID(ClassID) 呢?
其实虽然看起来简单, 但是还得考虑一个可能, 同一个 student 对象可能多次访问 student.ClassName, 这么写岂不是每次访问都会执行 Class.FindByID?
因此, XCode 设计了扩展集合 Extends, 可以认为是一个字典, 每个扩展属性都经过它走一遭, 如果查询过一次就缓存起来, 避免反复查询.
Extends.Get 第一个属性是扩展属性名, 决定是否有缓存, 第二个是没有缓存时要执行的委托.
这就是扩展属性缓存, 默认缓存时间 10 秒, 足够抗住短期内成千上万次重复调用.
扩展属性优化
尽管有 Extends 扩展属性缓存支持, 但每个对象还是要执行一次 Class.FindByID 查询, 损耗还是不小的.
在 XCode 里面, 根据主键而设计的查询 (如 FindByID) 往往带有很好的缓存优化.
如上, 这是 XCode 默认生成的代码, 当 Class 表数据不足 1000 行时, 走实体缓存.
也就是说, Meta.Cache 时执行一次 select * from student 返回所有行, 并缓存起来. 后面的 Find 实际上是在缓存中查找. 实体缓存有效期默认 10 秒.
只有数据表达到 1000 行, 才走 Find(_.ID==id) 数据库查询 select * from class where id=? . 然而 XCode 下层还有一个数据层缓存, 相同 select 查询默认缓存 10 秒
此外, 也可以根据业务特点采用单对象缓存, 例如跨境电商的产品种类特别多(10 万 +), 可以采用字典式的单对象缓存.
因此, 在学生类那边看起来访问属性会触发多次 Class.FindByID, 殊不知它内部别有洞天, 三级缓存 (实体缓存, 对象缓存, 数据缓存) 等着伺候!(后续专文介绍缓存)
回到开头的例子, 一个列表页显示 20 个学生, 理论查询次数 1+20 次, 在多级缓存加持的扩展属性下, 99.99% 的时候只会查询 1 次, 而班级表的关联, 完全在内存缓存中进行.
一次简单的单表查询, 显然要比 join 班级表的查询要快得多!
魔方的特别支持
在上述扩展属性中, 注意到 ClassName 属性上有一个 Map 特性.
它表示映射, 本对象的 ClassID 字段, 映射到 Class 类的 ID 字段.
在魔方列表页中, 本来显示冷冰冰 ClassID 的地方, 就会变为显示友好的 ClassName.
在魔方表单页中, 本来显示数字框 ClassID 的地方, 也会变成显示下拉列表框.
如果下拉列表库内容很多, 可以精简 Map 特性, 只要第一个参数指明本地字段, 而不需要第二第三字段表示的目标字段. 此时在魔方表单页会显示数字框, 但是后面显示 ClassName
到此, 你还认为多次查询一定比单次 Join 慢吗?
系列教程
NewLife.XCode 教程系列[2019 版]
增删改查入门. 快速展现用法, 代码配置连接字符串
数据模型文件. 建立表格字段和索引, 名字以及数据类型规范, 推荐字段(时间, 用户, IP)
实体类详解. 数据类业务类, 泛型基类, 接口
功能设置. 连接字符串, 调试开关, SQL 日志, 慢日志, 参数化, 执行超时. 代码与配置文件设置, 连接字符串局部设置
反向工程. 自动建立数据库数据表
数据初始化. InitData 写入初始化数据
高级增删改. 重载拦截, 自增字段, Valid 验证, 实体模型(时间, 用户, IP)
脏数据. 如何产生, 怎么利用
增量累加. 高并发统计
事务处理. 单表和多表, 不同连接, 多种写法
扩展属性. 多表关联, Map 映射
高级查询. 复杂条件, 分页, 自定义扩展 FieldItem, 查总记录数, 查汇总统计
数据层缓存. Sql 缓存, 更新机制
实体缓存. 全表整理缓存, 更新机制
对象缓存. 字典缓存, 适用用户等数据较多场景.
百亿级性能. 字段精炼, 索引完备, 合理查询, 充分利用缓存
实体工厂. 元数据, 通用处理程序
角色权限. Membership
导入导出. xml,JSON, 二进制, 网络或文件
分表分库. 常见拆分逻辑
高级统计. 聚合统计, 分组统计
批量写入. 批量插入, 批量 Upsert, 异步保存
实体队列. 写入级缓存, 提升性能.
备份同步. 备份数据, 恢复数据, 同步数据
数据服务. 提供 RPC 接口服务, 远程执行查询, 例如 SQLite 网络版
大数据分析. ETL 抽取, 调度计算处理, 结果持久化
来源: https://www.cnblogs.com/nnhy/p/xcode_extend.html