ORM架构不是一张图,是一条路。 你只有走在路上,才知道下一步该怎么走。

发布时间:2026/8/9 1:37:51
ORM架构不是一张图,是一条路。 你只有走在路上,才知道下一步该怎么走。 ORM架构不是一张图是一条路。你只有走在路上才知道下一步该怎么走。——从2004年的一条SQL到今天的IEntity契约与异步门面作者长江支流 日期2026-08-08关键字架构、ORM、IEntity、重构、跨语言关注本博客开源轻量架构与ORM、即将上传到csdn代码仓https://gitcode.com/ZeroORM和同名Github敬请关注一、2004年重复劳动催生第一个接口2004年.NET Framework 1.1没有泛型没有依赖注入。我每天做的事很简单写SQL拼SQL再写SQL。INSERT、UPDATE、DELETE、SELECT每个表都要写一遍。表多了代码就多了字段多了拼的字符串就长了。后来我发现不管操作哪个表无非就是四样东西表名、主键、字段列表、字段值。我写了一个简单的抽象类让每个实体自己告诉我这四样东西。当时没有泛型我就用IList没有依赖注入我就用抽象方法让子类自己提供IExeSql执行器——这就是今天依赖注入的前身。publicclassEntityTest:WebMIS.Data.EntityAccess.DBEntity{privateint_ID-1;privatestring_Nametest;publicintID{get{return_ID;}set{_IDvalue;}}publicstringName{get{return_Name;}set{_Namevalue;}}publicEntityTest():base(TableNameOfEntityTest,ID){}publicoverrideIListGetFields(){returnnewstring[]{ID,Name};}publicoverrideIListGetFieldValues(){returnnewobject[]{_ID,_Name};}publicoverrideIListGetPrimaryKeyValues(){returnnewstring[]{ID};}// ★ 当时直接依赖 DataRowpublicoverridevoidLoadFrom(System.Data.DataRowentityDataRow){_IDint.Parse(entityDataRow[ID].ToString());_NameentityDataRow[Name].ToString();}}配套一个EntityManager来管理实体的所有操作执行增删改查。这段代码现在看很原始但当时它解决了问题写一次实体所有CRUD操作自动完成。二、2007年被两个问题逼着往前走2007年我遇到了两个新问题第一个问题加字段就要重新编译。客户说“加一个字段”。你要改实体类、改GetFields()、改LoadFrom()然后重新编译、重新部署。有没有办法不改代码、不重新编译就能升级第二个问题100个表写100个实体类如果每个表都要写一个实体类每个类都要实现GetFields()、GetFieldValues()、LoadFrom()那这个框架就回到了原来的老路。能不能让程序自动生成实体当时我给出的答案是用XML描述实体运行时解析。结构改变只需要手动或自动更新XML不需要重新编译。这个想法后来变成了XmlMapEntity——一个由XML配置驱动的动态实体。它不是一开始就设计好的是被问题“逼”出来的。我在2007年的博客里写了三篇文章《架构是什么架构就是实践》《架构是什么架构就是总结》《架构是什么架构就是创新》这三篇文章的核心观点是架构是实践架构是总结架构是创新。但当时我写这些文章的时候其实只做了一件事——把实际遇到的问题记下来然后想办法解决。三、关键转折从DataRow隔离到跨语言早期版本中LoadFrom直接依赖DataRowpublicoverridevoidLoadFrom(DataRowentityDataRow){_IDint.Parse(entityDataRow[ID].ToString());_NameentityDataRow[Name].ToString();}后来我发现DataRow是ADO.NET的一部分而ADO.NET在不同版本中行为不完全一致。更麻烦的是如果将来跨语言Java、ArkTS根本没有DataRow这个东西。一旦依赖DataRow这条跨语言的路就彻底堵死了。我加了一层隔离publicinterfaceIEntityFieldProvider{objectGetValue(stringfieldName);}LoadFrom不再依赖DataRow只依赖这个接口publicoverridevoidLoadFrom(IEntityFieldProvidersource){_IDConvert.ToInt32(source.GetValue(ID));_Namesource.GetValue(Name)?.ToString();}底层不管是DataReader、DataRow、ResultSet、JsonObject还是其他什么只要实现了IEntityFieldProvider就能填充实体。这就是“依赖隔离”的思路框架只依赖接口不依赖具体实现。这个原则后来贯穿了整个用宝框架的设计——IDataAccessExecutor隔离数据库操作、IEntityParser隔离配置解析、IOrmProvider隔离具体ORM引擎。每一个接口的存在都是为了防止“换一个环境就带不过去”。四、弃用重资产、拥抱轻量化的设计原则正因为当年面对DataRow依赖的困境我逐步建立了一条明确的设计原则层级弃用的重资产替换方案设计原则数据填充DataRowIEntityFieldProvider框架只依赖接口不依赖具体实现数据库操作SqlCommand/DbCommandIDataAccessExecutor隔离数据库差异跨库不跨逻辑实体映射EF/DataSetIMapEntityTKey实体自描述不依赖任何ORM框架配置驱动硬编码实体类XmlMapEntity 解析器配置驱动不改代码不重新编译这不是技术偏好而是生存策略——用宝框架的代码要能在.NET、Java、ArkTS三个环境里跑必须保持接口抽象必须放弃任何“某个平台独有”的东西。EF很好但它只属于.NETDataRow很方便但它带不到JavaDataSet很强大但它跨不了语言。我们不是在做“又一个ORM框架”而是在构建一个“能跨语言迁移的轻量化数据访问基座”。五、今天从接口到契约从同步到异步从2004年到现在二十多年过去了。当初那个没有泛型的IEntityMap接口今天变成了支持复合主键的IEntityTKey只保留Id属性是最底层的实体契约publicinterfaceIEntityTKey{TKeyId{get;set;}}当初那个手动实现GetFields()的实体基类今天演变成了三个并存的实现方式实体类元数据来源适用场景AttributeMapEntity特性反射[Table]/[Column]静态实体编译时确定ManualMapEntity构造函数手动传入手写映射灵活可控XmlMapEntity解析器运行时注入动态实体配置驱动当初那个用IList凑合用的接口今天变成了跨语言通用的IMapEntityTKeypublicinterfaceIMapEntityTKey:IEntityTKey{stringTableName{get;}string[]PrimaryKeys{get;}string[]GetFields();object[]GetFieldValues();object[]GetPrimaryKeyValues();voidLoadFrom(IEntityFieldProvidersource);}当初那个用抽象方法让子类提供IExeSql的注入方式今天变成了标准的依赖注入并通过门面模式对外只暴露IEntity接口publicinterfaceIOrmProviderTKey{voidSetEntityT(Tentity)whereT:IEntityTKey;TKeyInsert();intUpdate();boolDelete();IEntityTKeyGetById(TKeyid);IDataListIEntityTKeyGetList(IQueryParametersquery);intGetTotal(IQueryParametersquery);}而为了应对高并发场景我们又增加了异步接口与同步接口并行存在publicinterfaceIOrmAsyncProviderTKey{TaskSetEntityAsyncT(Tentity)whereT:IEntityTKey;TaskTKeyInsertAsync();TaskintUpdateAsync();TaskboolDeleteAsync();TaskIEntityTKeyGetByIdAsync(TKeyid);TaskIDataListIEntityTKeyGetListAsync(IQueryParametersquery);TaskintGetTotalAsync(IQueryParametersquery);}同步接口保持跨语言通用C#/Java/ArkTS异步接口作为C#专属高并发优化。两者互不干扰相辅相成。六、架构演进的核心思想从继承到接口从接口到契约纵观这二十多年的演进核心就一句话接口解决的是“谁来定义”的问题契约解决的是“为什么能替换”的问题。阶段形式解决的问题抽象类实体继承基类提供元数据让实体自己描述自己接口实体实现接口框架依赖接口让实体和框架解耦契约接口跨语言统一三端一致让实体能在不同平台间迁移因为底层只依赖IEntity所以上层的所有功能——ORM提供者、万能控制器、门面封装、跨语言迁移——都可以围绕这个契约自由生长而不受具体实体实现的限制。七、实体访问与ORM提供者IOrmProvider 将底层复杂的操作封装让使用者更方便。IMapEntityAccess 已经能完成所有 CRUD 操作但它执行具体类型 TEntityIOrmProvider 在它之上包了一层把所有复杂细节藏起来只留给调用方一个简单的接口——设置实体然后操作。这就是门面设计模式的价值。区别对比维度IMapEntityAccessTEntityIOrmProviderTKey层级ORM 层数据访问层门面层对外接口层状态管理有状态增删改依赖Entity属性有状态增删改依赖SetEntity设置的实体实体设置方式Entity属性直接赋值SetEntityT(T entity)方法查询方法DoRead/GetTotalCount需显式传参GetList/GetTotal基于已设置的实体面向对象面向具体实体类型TEntity面向接口IEntityTKey返回类型具体类型TEntity、IDataListTEntity接口类型IEntityTKey、IDataListIEntityTKey使用场景Access 层内部使用对外暴露给 Controller隐藏内部实现关系总结// IMapEntityAccess增删改依赖 Entity 属性查询显式传参publicinterfaceIMapEntityAccessTEntity{TEntityEntity{get;set;}// ← 设置实体intInsert();// ← 操作 EntityintUpdate();// ← 操作 EntityintDelete();// ← 操作 EntityboolFillByPK();// ← 操作 EntityIDataListTEntityDoRead(IQueryParametersquery,TEntityentity);// 显式传参intGetTotalCount(IQueryParametersquery,TEntityentity);// 显式传参}// IOrmProvider所有操作都基于 SetEntity 设置的实体publicinterfaceIOrmProviderTKey{voidSetEntityT(Tentity)whereT:IEntityTKey;// 设置实体TKeyInsert();// ← 基于已设置的实体intUpdate();// ← 基于已设置的实体boolDelete();// ← 基于已设置的实体IEntityTKeyGetById(TKeyid);// ← 基于已设置的实体IDataListIEntityTKeyGetList(IQueryParametersquery);// ← 基于已设置的实体intGetTotal(IQueryParametersquery);// ← 基于已设置的实体}本质区别IMapEntityAccess和IOrmProvider都是有状态的区别在于对比项IMapEntityAccessIOrmProvider实体设置方式属性赋值Entity entity方法调用SetEntity(entity)查询参数查询需显式传entity因为同一个 Access 实例可能被复用查不同实体查询基于已设置的实体因为 Provider 设计为“设置一次复用多次”对外暴露TEntity具体类型IEntityTKey接口一句话总结IMapEntityAccess是“会记住当前实体的执行器”IOrmProvider是“会记住当前实体的门面”。区别在于一个暴露具体类型一个暴露接口一个内部用一个对外用。✅八、给后来者的一句话不要迷信于别人的架构它有参考价值但决不会是一成不变的。任何一个架构在一定条件下可能是优秀的架构而在另一个条件和应用中可能它就是个有缺陷的架构。架构不是一张图是一条路。你只有走在路上才知道下一步该怎么走。如果你现在写的代码让你觉得“重复”“繁琐”“改起来很麻烦”那说明你正在经历架构演进的第一步——实践。接下来你要做的就是总结和创新。架构是实践架构是总结架构是创新。附录架构演进时间线时间里程碑说明2004年IEntityMapEntityManager.NET 1.1无泛型无DI靠抽象方法注入执行器2007年XML配置驱动思想不编译程序改XML升级2007年IEntityFieldProvider隔离DataRow依赖为跨语言铺路2010年代IEntityTKeyIMapEntityTKey泛型支持复合主键现在IOrmProviderTKey门面对外只暴露IEntity隐藏内部实现现在IOrmAsyncProviderTKey同步异步双接口覆盖高并发附录有人问实体访问和ORM提供者好像基本相类似为何不统一这个问题问到根子上了。这两个接口有重叠但不完全对应是因为它们的职责不同。直接回答对比项IMapEntityAccessTEntityIOrmProviderTKey定位ORM 数据访问层执行者业务门面层封装者返回 Insertint影响行数TKey插入后的主键返回 Updateint影响行数int影响行数返回 Deleteint影响行数bool是否成功查询方法DoRead显式传参GetList基于已设置的实体获取总数GetTotalCount显式传参GetTotal基于已设置的实体单条查询FillByPK填充当前实体GetById返回新实体为什么不统一因为使用场景不一样。IMapEntityAccess是数据访问层它关心的是“数据库操作的结果”——插入了多少行、删除是否影响到了数据。所以它返回int是直白的。IOrmProvider是业务门面层它关心的是“业务操作的结果”——插入后新记录的主键是什么、删除是否成功。业务层通常不需要知道影响行数只需要知道“成没成”。举个例子// Access 层我需要知道插入了多少行introwsaccess.Insert();// 1 表示成功0 表示失败// Provider 层我需要知道新记录的 IDstringidprovider.Insert();// PROD-001业务层需要这个 ID 做后续操作Update两者都返回int是因为更新操作确实有“0 行更新”的情况——记录不存在或没有变化这在业务层是有用的。Delete在 Access 层返回int在 Provider 层返回bool是因为业务层通常只关心“删除成功与否”不需要知道影响行数。一句话总结IMapEntityAccess告诉调用者“操作执行得怎么样”IOrmProvider告诉调用者“操作产生了什么结果”。一个面向执行细节一个面向业务结果。