MyBatis-Plus 已经很好用我为什么还要自研 ORM摘要MyBatis 擅长把 SQL 与 Java 对象连接起来MyBatis-Plus 又补齐了通用 CRUD 和 Lambda 条件为什么 MetaLite 仍要自研 ORM原因不是重复造一个更大的框架而是希望把类型安全、MySQL 与 Elasticsearch 高频语义、多数据源路由、事务时序和复杂 SQL 逃生口放进同一套可控契约。本文结合backend-orm源码说明这套取舍也明确它不准备替代 MyBatis 的全部场景。我长期使用过 MyBatis也使用过 MyBatis-Plus。它们能解决问题生态成熟团队也容易招聘到熟悉的开发者。尤其是复杂 SQLMyBatis 让开发者保留了足够直接的控制力MyBatis-Plus 的通用 CRUD 和 Lambda Wrapper也显著减少了重复代码。但在持续开发企业级项目时我越来越在意另一组问题简单单表操作为什么仍要在 Mapper、XML、Service 之间来回切换字段改名后字符串列名和 SQL 片段为什么很难被 IDE 完整重构多数据源为什么逐渐演变成注解、字符串、切面和 ThreadLocal 的组合MySQL 与 Elasticsearch 为什么让业务层维护两套完全不同的查询模型事务为什么总在业务方法入口就启动而数据源路由可能到第一次 DAO 调用才确定自研 DSL 一旦遇到联表和窗口函数是否又会变成另一种更难读的“SQL”。MetaLite ORM 就是在这些问题下形成的。它不是为了证明 MyBatis 不好而是为了让 MetaLite 的数据访问层拥有一套更小、更一致、边界由自己控制的工程契约。一、先说结论MyBatis 与 MetaLite ORM 解决的问题不完全相同可以把二者的主要关注点简化为方案更擅长的问题MyBatisSQL 映射、复杂 SQL、存量数据库和精细控制MyBatis-Plus在 MyBatis 之上补充通用 CRUD、Wrapper 和常用插件MetaLite ORM统一高频数据访问契约、组件路由、类型安全与跨存储复用MetaLite ORM 没有试图创造一门覆盖全部数据库能力的新语言而是把数据访问分成两条路高频单表操作 → BaseEntityDao Criteria / Query / Update 复杂 SQL → BaseSqlDao 显式 SQL这条分界是整个设计的核心。如果把源码能力放在同一张表里差异会更清楚能力MyBatis / MyBatis-Plus 的典型方式MetaLite ORM 的选择通用 CRUDMapperMyBatis-Plus BaseMapperBaseEntityDao统一 insert、update、delete、exists、count、findOne、findList类型安全条件XML/字符串MyBatis-Plus Lambda WrapperCriteria同时支持字符串和方法引用按需返回字段手写 SELECT 或 Wrapper selectQuery.includeField/excludeField用于 findOne、findList 与分页MySQL 与 Elasticsearch通常维护两套客户端和条件模型common 层复用BaseEntityDao、Criteria、Query多数据源多 SqlSession 或动态数据源组件数据源 group、master/slave、Dao绑定与DbRouter分库分表插件、中间件或业务代码DbRouter与TableRouter分开扩展本地事务Transactional最常见只提供编程式入口第一次 DAO 延迟绑定 DataSource事务内读一致性依赖动态数据源与事务配置QueryToMasterSwitch强制事务内读取主库字段更新动态 SQL 或 UpdateWrapperinclude、exclude、按 id/ids/Criteria 更新和实体差异比较自增主键generatedKeys 配置单条与批量回填并校验批量主键模式复杂 SQLMyBatis 核心优势BaseSqlDao保留原生 SQL 逃生口DAO 观测日志、拦截器或监控插件DaoCallAspect、DaoCallLogger预留统一入口这张表不表示 MetaLite 在所有维度上“功能更多”。例如 MyBatis 对复杂 SQL 的控制力和生态成熟度明显更强MetaLite 的重点是让高频能力能够在同一套工程契约里组合。还要公开三条边界TableRouter不是 SQL 解析与跨分片执行引擎本地事务绑定后只覆盖一个实际 DataSourceCriteria当前不处理联表、子查询和 OR 组合。超出边界时应回到原生 SQL、成熟分片中间件或分布式一致性方案。二、第一个痛点简单 CRUD 不应该要求业务层理解太多基础设施MyBatis 项目常见结构是Controller → Service → Mapper 接口 → XML / 注解 SQL对于复杂查询这种分层很合理但对按主键查询、条件列表、分页、更新指定字段等高频操作大量结构只是在重复描述实体和表之间的关系。MetaLite 把这些稳定操作收进BaseEntityDaoTTfindOneById(Serializableid);ListTfindListByCriteria(Criteriacriteria);intupdateByCriteria(Criteriacriteria,Updateupdate);intdeleteByCriteria(Criteriacriteria);longcountByCriteria(Criteriacriteria);JDBC 实现由BaseEntityJdbcDaoT承担业务 DAO 只需要继承并声明实体类型Dao(dataSourceGroupmain)publicclassUserDaoextendsBaseEntityJdbcDaoUserEntity{}框架从泛型、Table、Column和主键元数据中生成高频 SQL再交给 Spring JDBC 使用占位符执行。它减少的不是一条 SQL而是简单需求在多个文件之间跳转的理解成本。三、第二个痛点字段字符串无法获得完整重构保护MyBatis XML、注解 SQL 和普通字符串条件都可能出现user_name userName userName实体字段重命名时IDE 能修改 Getter 和方法引用却不能判断每一个字符串究竟是不是数据库字段。MyBatis-Plus 的 Lambda Wrapper 已经提供了一种成熟改善方式。MetaLite ORM 采用相同方向把可序列化方法引用下沉到公共条件模型Criteria.where(UserEntity::getStatus,1).like(UserEntity::getName,张%);EntityHelper.genFieldName通过SerializedLambda提取 Getter 对应的属性名UserEntity::getUserName → getUserName → userName必须说明MetaLite 当前仍保留字符串重载因为动态字段、ES 专有字段和某些框架场景无法全部使用方法引用。因此它是“优先类型安全”不是“彻底消灭字符串”。四、第三个痛点业务同时使用 MySQL 和 Elasticsearch许多项目的现实结构是MySQL → MyBatis / MyBatis-Plus Wrapper Elasticsearch → Java Client Query DSL两者能力当然不同但等于、范围、集合、空值、分页和排序等查询意图高度重复。如果业务层必须维护两套条件对象切换存储或做双写迁移时重复会非常明显。MetaLite 将公共契约放在backend-orm-commonCriteria Query Update Pageable OrderBy BaseEntityDaoJDBC 实现把 Criteria 翻译成占位符 SQLES 实现把相同高频条件翻译成 Elasticsearch Query。但这层统一有明确边界SQL LIKE 与全文检索不是同一种语义ES 地理查询没有必要伪装成关系数据库能力复杂聚合、关联和存储专有特性不能强行统一当前AggCriteria仍只是预留不能宣传为完整聚合能力。MetaLite 统一的是高频查询意图不是数据库本身。五、第四个痛点多数据源不只是选择一个连接池MyBatis 生态中多数据源通常通过注解、AOP 和 ThreadLocal 实现。这种方案很实用但当系统继续加入主从、分库和事务时业务代码可能逐渐知道太多路由细节。MetaLite 把路由拆成几个层次Dao.dataSourceGroup → 选择业务数据源组 → DbRouter 选择组内主库或从库 → JdbcTemplateManager 获取执行对象DAO 声明自己属于哪个数据源组读写类型决定主从方向分库规则由DbRouter实现事务内则强制回到主库避免同一事务读到复制延迟数据。这套设计没有消除路由复杂性而是把它从业务方法转移到可替换的基础设施边界。六、第五个痛点事务启动时间可能早于数据源路由声明式事务通常在进入 Service 方法时获取连接。但动态数据源场景中真正的路由键可能来自第一次 DAO 操作的用户、租户、城市或业务参数。如果事务先绑定默认 DataSource后续再切库已经太晚。MetaLite 的TransactionContext使用两阶段状态PENDING → 记录需要事务但暂不获取连接 BOUND → 第一次 DAO 调用确定 DataSource 后真正启动事务TransactionManager.doInTransaction只包围需要原子性的 DAO 操作而不是把远程调用、复杂计算和等待时间全部放进数据库事务。它的边界同样明确本地事务不能跨数据源跨数据源或跨服务一致性要进入 Seata 等分布式事务方案。七、自研 ORM 最容易犯的错假装能够表达所有 SQL如果 Criteria 最终长出联表、子查询、窗口函数、任意 OR 树、数据库 Hint 和所有方言它很可能变成一门比 SQL 更难学习的语言。MetaLite 的Criteria源码直接声明主要支持单表 AND 并列条件。遇到复杂场景使用BaseSqlDaoListEfindListBySql(Stringsql,LinkedHashMapString,ObjectparamMap,ClassEbeanClass);所以 MetaLite ORM 的策略不是“拒绝 SQL”而是简单操作不重复写 SQL 复杂操作不逃避写 SQL这一点实际上保留了 MyBatis 最重要的工程优点开发者在复杂查询上仍然拥有直接控制权。八、MetaLite ORM 的模块为什么拆成四部分backend-orm当前结构如下backend-orm-common → 公共 DAO、Criteria、Query、Update、路由接口 backend-orm-jdbc → Spring JDBC、SQL 生成、多数据源和本地事务 backend-orm-es → Elasticsearch 客户端、查询翻译和索引路由 backend-orm-seata → DataSourceProxy、XID 绑定与传播这种拆分让业务可以按需依赖只用 MySQL 不必引入 ES只用本地事务不必启用 Seata。公共契约负责稳定具体存储实现负责差异分布式事务作为可选能力接入。九、哪些项目更适合继续使用 MyBatis自研不等于适合所有人。以下场景继续使用 MyBatis 或 MyBatis-Plus 往往更合理团队已经有大量稳定 Mapper 和 XML核心工作以复杂 SQL、报表和存储过程为主团队依赖成熟插件生态没有 MySQL 与 ES 统一高频契约的需求不希望承担自研 ORM 的测试和维护成本。而 MetaLite ORM 更适合大量操作是单表高频 CRUD希望业务 DAO 保持统一接口需要多数据源、主从和路由边界同时使用关系数据库和 Elasticsearch愿意为可控的底座契约维护自己的实现。十、自研 ORM 的真正成本是什么写出一个findById并不难难的是长期保证SQL 参数始终使用预编译绑定null 字段和批量实体形状得到正确定义自增 ID 单条与批量回填可验证事务传播、隔离级别和超时符合预期主从路由与事务一致性不冲突ES 批量部分失败不会被当成整体成功新增操作符在 JDBC 与 ES 上有清晰语义复杂能力超出 DSL 后有明确逃生口。这也是为什么 MetaLite ORM 必须把“当前不支持什么”写进文章。自研框架的可信度不来自功能清单而来自边界是否诚实。十一、我为什么最终选择自己写使用 MyBatis 和 MyBatis-Plus 的经历让我更清楚成熟框架解决了什么也让我确认 MetaLite 想解决的是另一层问题不是让每一种 SQL 都更容易写而是让企业应用最常见的数据访问拥有统一、可追踪、可替换的工程边界。所以 MetaLite ORM 保留了类型安全的高频 DSL也保留了显式 SQL统一了 MySQL 与 ES 的公共意图也承认两种存储的差异封装了多数据源和事务时序也没有把本地事务宣传成分布式事务。它不是 MyBatis 的“全面替代品”而是 MetaLite 为自己的工程目标做出的底座选择。框架简介MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。源码基线JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3具体组件版本以项目backend-bom为准。作者简介15 年 Spring 体系企业级开发经验专注于 Java 微服务架构、工程治理与生产实践。持续更新MetaLite 系列内容将持续更新围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者及时获取后续内容。在线演示演示地址: https://admin.metalite.top/演示账号: guess演示密码: admin2026