MyBatis增删改查只是基础吗-MetaLite-ORM如何把CRUD做成工程能力

MyBatis 增删改查只是基础吗?MetaLite ORM 如何把 CRUD 做成工程能力

摘要: 谁说增删改查很基础?客服分配、支付回调、优惠券领取、会员权益、库存扣减,最终都要落到“查哪条数据、写哪些字段、在什么条件下更新、失败后如何保持一致”。真正的业务系统不是不用 CRUD,而是把 CRUD 推向了类型安全、字段投影、批量写入、差异更新、主从路由、分库分表和事务边界。MetaLite ORM 没有把 CRUD 当成几个生成方法,而是用 BaseEntityDaoCriteriaQueryUpdate、数据源路由与事务上下文,把高频数据操作组织成一套可组合、可约束、可扩展的工程能力。

“这不就是增删改查吗?”

这是业务开发中最容易被低估的一句话。

一个客服工单被谁领取,是查询与条件更新;一张优惠券能否发放,是资格查询、库存写入和幂等判断;一次支付回调能否推进订单,是状态查询、条件更新和事务;一个会员被注销,是删除、软删除、审计和关联数据保留策略。

业务规则可以写在流程图里,最终却一定要通过数据的读取和变化变成事实。

所以,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 提供 includeFieldexcludeField,同时支持字符串和方法引用,让投影成为通用查询的一等能力。

这里也有明确边界:字段投影后,未查询字段会保留 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 或自定义 SQLfindOnefindList、分页与默认结果上限统一
字段安全ResultMap、select 列或 Wrapper selectinclude/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,而应该看它是否帮助团队稳定回答下面的问题:

  1. 这次操作会命中哪个数据源和物理表?
  2. 查询到底返回一个、多个,还是受保护的分页结果?
  3. 哪些字段会被读取和修改?
  4. 并发条件是否进入最终 SQL?
  5. 更新和删除影响行数是否被业务检查?
  6. 事务绑定了哪个真实数据源?
  7. 跨库、复杂 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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值