MyBatis 增删改查只是基础吗?MetaLite ORM 如何把 CRUD 做成工程能力
摘要: 谁说增删改查很基础?客服分配、支付回调、优惠券领取、会员权益、库存扣减,最终都要落到“查哪条数据、写哪些字段、在什么条件下更新、失败后如何保持一致”。真正的业务系统不是不用 CRUD,而是把 CRUD 推向了类型安全、字段投影、批量写入、差异更新、主从路由、分库分表和事务边界。MetaLite ORM 没有把 CRUD 当成几个生成方法,而是用
BaseEntityDao、Criteria、Query、Update、数据源路由与事务上下文,把高频数据操作组织成一套可组合、可约束、可扩展的工程能力。
“这不就是增删改查吗?”
这是业务开发中最容易被低估的一句话。
一个客服工单被谁领取,是查询与条件更新;一张优惠券能否发放,是资格查询、库存写入和幂等判断;一次支付回调能否推进订单,是状态查询、条件更新和事务;一个会员被注销,是删除、软删除、审计和关联数据保留策略。
业务规则可以写在流程图里,最终却一定要通过数据的读取和变化变成事实。
所以,CRUD 并不低级。CRUD 是业务规则落地到数据世界的最后一公里,也是并发、性能、一致性和线上故障最集中的地方。
MetaLite ORM 想解决的正是这个长期被称为“基础代码”的核心问题:不只让开发者能够增删改查,而是让每一种常见数据操作都有清晰语义、合理约束、统一路由和必要的扩展出口。
一、业务系统的核心,往往就是把一次 CRUD 做对
先看几个真实业务动作:
| 业务动作 | 表面需求 | 数据层真正要回答的问题 |
|---|---|---|
| 客服领取工单 | 把工单分给客服 | 工单是否待领取?是否只能一人成功?更新哪些字段? |
| 支付回调 | 把订单改成已支付 | 回调是否重复?旧状态是否允许推进?订单与流水是否同一事务? |
| 发放优惠券 | 给用户增加一张券 | 是否满足资格?是否重复领取?库存如何扣减?失败如何补偿? |
| 修改会员资料 | 保存编辑结果 | 未提交字段能否被覆盖?敏感字段是否允许修改? |
| 删除离职员工 | 删除一条记录 | 是物理删除还是停用?历史审计和关联业务是否必须保留? |
| 查询知识库 | 返回用户可见内容 | 查哪些字段?权限条件在哪里注入?是否需要数据库与搜索引擎协同? |
如果 ORM 只提供 insert()、update()、delete() 和 select() 四个方法,真正困难的部分仍会散落在 Service、XML、拦截器和工具类中。
MetaLite ORM 对“做好 CRUD”的定义更具体:
查询 = 条件语义 + 返回基数 + 字段投影 + 排序分页 + 读路由
新增 = 字段映射 + 批量边界 + 主键回填 + 写路由 + 分表路由
更新 = 更新范围 + 业务条件 + 影响行数 + 事务 + 并发语义
删除 = 删除范围 + 业务审计 + 路由 + 事务 + 风险控制
这也是“把增删改查做到极致”更可信的解释:不是宣称覆盖全部 SQL,而是把业务开发中最高频、最容易出错的变化维度变成显式能力。
二、查询不是 SELECT *,而是对结果边界的精确表达
业务代码说“查一下”,至少可能有四种不同语义:
- 按主键查一个对象;
- 按条件查第一个对象;
- 按条件查一个列表;
- 按条件分页,并且只返回指定字段。
BaseEntityDao 为这些语义提供不同入口:
findOneById(id);
findOneByCriteria(criteria);
findOneByQuery(query);
findListByIds(ids);
findListByCriteria(criteria);
findListByQuery(query);
findListByQuery(query, pageable);
findOneByQuery 会把查询收敛为 offset(0).limit(1),没有结果返回 null;列表查询则返回列表。调用方不需要每次手工判断 list.get(0),也不需要把“查一个”和“查多个”的业务语义藏在同一个返回类型里。
查询对象 Query 还把条件、字段、排序、分组、偏移量、数量和 hint 组织在一起:
Query query = Query.query(
Criteria.where(OrderEntity::getUserId, userId)
.eq(OrderEntity::getStatus, status)
).includeField(
OrderEntity::getId,
OrderEntity::getStatus,
OrderEntity::getCreateTime
).limit(100);
这段代码表达的不只是“按用户查询订单”,还表达了:只需要哪些字段、最多返回多少条、调用方期待什么数据形态。
字段投影尤其重要。大量接口只需要 ID、状态和时间,如果所有查询默认加载完整实体,不仅浪费网络和对象转换成本,还会扩大字段误用与敏感数据暴露的范围。MetaLite 提供 includeField 和 excludeField,同时支持字符串和方法引用,让投影成为通用查询的一等能力。
这里也有明确边界:字段投影后,未查询字段会保留 Java 默认值,调用方不能把它当成数据库真实值;空投影当前也不会自动回退为 SELECT *。
三、更新最危险的不是 SQL 写错,而是“更新了不该更新的字段”
前端提交一份编辑表单,并不代表数据库实体的所有字段都应该被覆盖。
例如用户只修改昵称,反序列化后的对象里,手机号、状态、权限字段可能是 null 或默认值。如果直接全字段更新,就可能把“没有提交”误判为“要清空”。
MetaLite ORM 为不同更新意图提供了多层入口:
update(entity) 按实体主键更新
updateIncludeFields(entity, fields) 只更新指定字段
updateExcludeFields(entity, fields) 排除禁止更新字段
updateById(id, update) 按一个主键更新指定值
updateByIds(ids, update) 按多个主键批量更新
updateByCriteria(criteria, update) 按业务条件更新
Update 可以通过方法引用明确设置字段:
Update update = Update.update(OrderEntity::getStatus, newStatus)
.set(OrderEntity::getUpdateTime, now);
int affected = orderDao.updateByCriteria(
Criteria.where(OrderEntity::getId, orderId)
.eq(OrderEntity::getStatus, oldStatus),
update
);
这里真正有价值的不是少写几行 SQL,而是把业务前置条件一起放进更新:只有订单仍处于旧状态时,状态推进才成功。
随后检查 affected 是否为 1,才能判断本次状态迁移是否真实发生。这比“先查状态、再无条件更新”更接近并发环境中的正确语义。
UpdateHelper 还能从实体生成 include 或 exclude 更新集合,让“只更新提交字段”和“禁止覆盖系统字段”不必为每个 DAO 重写动态 SQL。
但 MetaLite 当前 Update 主要支持 SET field = value,并不等于已经具备数据库原子自增、复杂表达式或乐观锁插件。库存扣减等场景仍要使用带业务条件的原生 SQL,或者继续扩展 Update 语义,不能把普通 set 更新宣传成全部并发控制。
四、新增不只是保存对象,还包括批量边界和主键生命周期
单条插入最常见的问题之一,是数据库生成主键以后,业务对象是否立即拿到这个 ID。
BaseEntityJdbcDao.insert(entity) 会识别自增主键,通过 GeneratedKeyHolder 获取数据库返回值,转换为实体字段类型后回填对象。调用方可以继续用同一个实体创建关联关系,而不必再查询一次数据库。
批量插入则不是把单条插入循环调用。当前 JDBC 实现使用批处理,并明确限制:
- 目前只支持 MySQL 批量插入路径;
- 单次最多插入 2000 条;
- 批次中的实体需要保持一致的字段与主键模式;
- 需要自增主键时,批量结果仍要与原实体逐一对应。
这些限制不是“不够灵活”,而是将容量和结果映射的不确定性显式暴露。一个没有上限的批量 API 看起来更自由,却可能在生产环境把大请求直接变成超长 SQL、内存峰值和连接占用。
新增操作还会经过 insertRoute 选择物理表,再从数据源组取得写库连接。业务 DAO 调用的是一个 insert,底层同时处理了字段映射、表路由、写路由、参数绑定、异常转换和主键回填。
五、删除越简单,越应该对它保持警惕
MetaLite ORM 提供两类通用删除:
deleteById(id);
deleteByCriteria(criteria);
按 ID 删除适合边界明确的物理记录;按条件删除适合清理某一批符合条件的数据,并且会进入 deleteRoute 选择目标表。
但一个框架不应该替业务决定“删除”的含义。订单、支付、工单、会员等实体通常不能因为接口叫 delete 就直接物理删除。停用、注销、归档、软删除、匿名化和法定保留期是领域规则,必须由业务模块定义。
尤其需要注意:当前 deleteByCriteria 要求 Criteria 非空,但框架不能仅凭方法名称判断条件是否足够收敛,也不会自动替项目完成二次确认、审计、备份和恢复。高风险批量删除仍应在 Service 层增加权限、数量阈值、审计与事务保护。
把删除做到工程化,不是让删除更方便,而是让开发者知道它会删什么、在哪个库和表执行、由谁授权、能否恢复。
六、一套 CRUD 契约,为什么还要连接路由与事务
如果 DAO 方法只负责生成 SQL,那么主从、多数据源、分库分表和事务仍会渗透到业务代码。
MetaLite ORM 把这些横向能力放在 CRUD 执行链中:
业务 DAO 方法
↓
Criteria / Query / Update 描述意图
↓
DbRouter 选择数据源候选实例
↓
TableRouter 选择物理表或索引
↓
JdbcTemplateManager 选择读库或写库
↓
TransactionContext 决定是否绑定事务连接
↓
JDBC / Elasticsearch 执行与异常转换
写操作默认走 master,普通查询可以读取 slave;事务开始以后,查询会通过 QueryToMasterSwitch 回到主库,避免刚写入的数据因为主从延迟而立即查不到。
DAO 通过 @Dao(dataSourceGroup=...) 绑定数据源组,组内再由 DbRouter 选择实际实例;表和 Elasticsearch 索引则可通过 TableRouter 按增删改查分别路由。于是“查、写、删哪一份数据”不再散落成 Service 中的字符串判断。
本地事务采用显式的编程式 TransactionManager,并在首次 DAO 调用时延迟绑定实际数据源。这种做法牺牲了一部分 @Transactional 的便利,却把动态数据源选择与事务开启顺序放在同一设计里。
它的边界同样必须说清楚:一个本地事务只绑定一个实际 DataSource,跨库原子性仍需要 Seata、消息最终一致性或业务补偿,不能因为 API 看起来统一就忽略物理边界。
七、与 MyBatis、MyBatis-Plus 相比,MetaLite 的差异在哪里
MyBatis 的优势是 SQL 控制力强,复杂联表、数据库特性和精细调优都可以显式表达;MyBatis-Plus 已经提供 BaseMapper、Wrapper、Lambda 条件和通用 CRUD,绝不是“只能手写 XML”。
MetaLite ORM 的差异不在于发明了 insert 或 findList,而在于把下面这些能力放进同一套高频契约:
| 维度 | MyBatis / MyBatis-Plus 典型做法 | MetaLite ORM 的重点 |
|---|---|---|
| 查询语义 | Mapper 方法、Wrapper 或自定义 SQL | findOne、findList、分页与默认结果上限统一 |
| 字段安全 | ResultMap、select 列或 Wrapper select | include/exclude 与方法引用直接进入 Query |
| 更新范围 | 动态 SQL、Wrapper update | 全量、include、exclude、ID、ID 集合与条件更新统一 |
| 存储差异 | MySQL Mapper 与 ES Client 通常分开 | JDBC 与 ES 复用 BaseEntityDao、Criteria、Query 高频语义 |
| 数据路由 | 插件、动态数据源或业务选择 | 数据源组、主从、DbRouter、TableRouter 与 DAO 执行链结合 |
| 事务选择 | @Transactional 为主 | 编程式事务与首次 DAO 数据源选择配合 |
| 复杂 SQL | 原生强项 | 通过 BaseSqlDao 主动保留逃生口 |
因此,MetaLite ORM 不是要全面替代 MyBatis 生态,而是针对企业项目中大量重复的单表高频操作,建立一套从表达、执行到路由和事务都相互配合的约定。
如果项目主要是复杂报表、多表关联、嵌套子查询、存储过程或强依赖 MyBatis 插件生态,继续使用 MyBatis 往往更合适;如果系统大量时间消耗在重复 Mapper、动态 SQL、多数据源选择和 JDBC/ES 两套访问模型上,MetaLite 的设计才更有价值。
八、“做到极致”不是没有边界,而是把边界也设计进去
MetaLite 的 Criteria 主要服务单表 AND 并列条件。OR、联表、嵌套查询和子查询没有被强行塞进越来越复杂的链式 DSL,而是交给 BaseSqlDao。
TableRouter 是物理表或索引选择扩展点,不是 ShardingSphere 一类 SQL 解析、跨分片执行和结果归并引擎;原生 SQL 也不会自动应用实体 DAO 的分表路由。
这种取舍恰恰说明了 MetaLite 对 CRUD 的态度:
高频路径要足够短、足够统一、足够安全;超出高频模型的复杂问题,应进入显式、专业、可观察的出口。
一个 ORM 是否成熟,不应该只看它能否用链式 API 拼出任意 SQL,而应该看它是否帮助团队稳定回答下面的问题:
- 这次操作会命中哪个数据源和物理表?
- 查询到底返回一个、多个,还是受保护的分页结果?
- 哪些字段会被读取和修改?
- 并发条件是否进入最终 SQL?
- 更新和删除影响行数是否被业务检查?
- 事务绑定了哪个真实数据源?
- 跨库、复杂 SQL 和分片场景何时应该退出通用模型?
把这些问题都留给每个开发者临时处理,CRUD 才会成为低水平重复劳动;把它们沉淀成一致的模型、约束和扩展点,CRUD 就成为技术底座最有价值的部分。
九、结语:业务创新,最终要靠可靠的数据变化落地
优秀的业务系统并不会因为技术先进就摆脱增删改查。恰恰相反,业务越复杂,对数据操作的精度要求越高。
MetaLite ORM 所追求的“极致”,不是用一个 DSL 覆盖数据库世界的所有可能,而是把企业业务最常走的路径反复打磨:查询有边界,字段可控制,更新有语义,批量有限制,主键有生命周期,读写会路由,事务有归属,复杂 SQL 有出口。
谁说增删改查很基础?
真正决定一个系统能否稳定运行、快速迭代和长期维护的,往往正是这些每天都会发生的数据操作。
框架简介
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
演示密码: admin@2026

510

被折叠的 条评论
为什么被折叠?



