从 MyBatis 到 JPA/Hibernate
在国内做 Java 开发,MyBatis 往往是我们很自然的选择。教程在讲,团队在用,接手的项目也大多如此。
我也问过一些开发同事为什么不使用 Hibernate,听到最多的回答是上手难、不好用。它确实有自己的学习成本。不过,这也让我想到另一个问题:为什么显式写 SQL 对我们来说很自然,而把对象的状态交给框架管理,反而不容易理解?
这可能和我们的学习路径有关。很多人从 SQL 和增删改查开始学习开发,也逐渐习惯了从数据库的角度理解需求:需要哪些表,要查询或更新哪些记录。我们熟悉了如何操作数据,却未必系统学过如何用对象表达业务规则。
当系统越来越复杂时,我们开始更关注业务规则如何落到代码里。尤其是在接触领域驱动设计(DDD)之后,看待开发的角度也会发生变化:开发应该围绕业务过程展开,数据库则负责保存这个过程产生的状态。
我们可以先从业务对象出发思考:订单有哪些状态,什么条件下可以付款,付款后会发生什么变化。这些规则可以通过对象及其行为来表达,数据库负责持久化相应的状态。这样,表结构就不再是组织业务代码的起点。
顺着这个思路,也就更容易理解为什么会有 ORM(对象关系映射)了。既然业务规则和状态变化已经在对象中表达,我们就需要一种机制,把对象及其关系映射到数据库,并把对象的变化同步保存下来。JPA/Hibernate 提供的正是这样的能力。它的意义不只在于省去几条 SQL,更在于让我们可以围绕业务对象完成一次操作,再由持久化机制处理数据的读写。
回头再看 MyBatis,就会发现它解决的是另一类问题。MyBatis 官方对自己的定位是 SQL mapper:开发者决定执行什么 SQL,它负责把对象参数传进去,再把查询结果映射成对象。虽然两者都涉及对象与数据库,但 SQL mapper 和 JPA/Hibernate 这样的 ORM,在工作方式上有根本区别:前者围绕显式的 SQL 操作,后者还会管理实体的生命周期、关联和状态同步。理解了这个区别,选型时就不会只比较谁的增删改查更方便。
从面向 SQL 到面向业务对象
在我们熟悉的 MyBatis 项目中,一次业务操作常常会写成一组数据库调用:
orderMapper.updateStatus(orderId, "PAID");
paymentMapper.insert(payment);
inventoryMapper.decrease(skuId, quantity);
这样的代码很容易看懂每一步改了什么数据。不过,随着业务变复杂,如果一直按这个方式往下写,规则就容易散落在 Service、Mapper 和 SQL 中。Order 只剩下字段和 getter/setter,关于订单的判断却分布在各处。修改一条规则时,我们需要找出所有相关的入口,才能确认有没有遗漏。
DDD 提供了另一种组织方式:把相关的状态和规则放到一起,让业务对象承担相应的行为。
在这个视角下,订单有自己的生命周期、状态转换和业务约束,可以被建模为一个聚合。处理订单付款,也就可以表达为订单上的一次业务行为:
Order order = orderRepository.get(orderId);
order.pay(payment);
调用 pay 时,订单负责判断当前状态是否允许支付、是否已经支付过、付款金额是否匹配,并在规则满足后改变自身状态。应用服务负责加载订单、调用业务方法,以及协调事务和其他必要的操作。这样读代码时,我们更容易看出业务在做什么,也更容易找到规则应该修改的位置。
MyBatis 同样可以用于这样的设计,只是我们需要在 Repository 的实现里显式完成对象的装载和保存。ORM 则进一步提供了对象状态管理能力,让这部分衔接更自然。
JPA/Hibernate 如何工作
JPA 是 Java 生态中的持久化标准,如今对应 Jakarta Persistence,Hibernate 是它的主流实现之一。除了对象与表的映射,它还提供实体生命周期管理、关联映射、级联、延迟加载和乐观锁等能力。其中,理解持久化上下文,是理解这套工作方式的关键。
可以把持久化上下文理解为一次工作中管理实体的空间。对于处于托管状态的实体,Hibernate 会跟踪它们的状态,并通过脏检查识别需要写入数据库的变化。一个常见的事务过程是:
- 应用服务开启事务,通过 Repository 加载聚合根及本次操作需要的数据。
- 加载出的实体由 Hibernate 的持久化上下文管理。
- 调用实体的业务方法,完成规则校验和状态变更。
- 在 flush 时,Hibernate 检查变化并执行相应的 SQL。
- 事务提交,确认这次数据库操作。
这套机制很适合承载 DDD 的写模型:在一个事务边界内加载聚合、执行业务行为、保存状态变化。不过,Hibernate 并不知道业务上的聚合边界在哪里。哪些对象属于一个聚合、哪些关联需要级联、哪里需要并发保护,仍然需要我们设计。
两种工具的差别也就清楚了。使用 MyBatis 时,我们通常先确定要执行哪些 SQL,再安排对象参数和结果映射;使用 JPA/Hibernate 时,我们更多地关注实体发生了哪些变化,由框架根据映射完成同步。这也是从面向数据库操作转向面向业务对象时,ORM 会变得更有吸引力的原因。
MyBatis-Plus 与 Hibernate 的区别在哪里
用过 MyBatis-Plus 的人可能会想到:它也有 save、getById、updateById、saveOrUpdate,常见操作已经不用手写 SQL 了,这与 Hibernate 还有什么区别?
区别主要在于,通用 CRUD 方法和实体状态管理解决的是不同的问题。
MyBatis-Plus 帮我们省去了大量重复的 Mapper 和 SQL 代码,日常开发会方便很多。不过,它仍然沿用 MyBatis 的基本工作方式:调用插入方法完成插入,调用更新方法完成更新。加载一个对象并修改字段后,仍然需要显式调用相应的持久化操作。
Hibernate 除了提供数据读写能力,还围绕实体建立了一套管理机制:
- 在同一个持久化上下文中,保持同一实体身份对应同一个受管理的对象实例;
- 跟踪托管实体的变化,通过脏检查在 flush 时同步状态;
- 按照关联映射和级联配置,处理相关实体的持久化操作;
- 通过版本字段支持乐观锁,检测并发修改;
- 通过延迟加载和抓取策略,控制关联数据的加载方式。
如果需求只是简化增删改查,MyBatis-Plus 已经很好用。如果希望在事务中操作业务对象,并由框架统一跟踪和保存状态变化,它就不能直接替代 Hibernate。
这些便利也有学习成本。使用 Hibernate,需要理解事务边界、关联加载、N+1 查询和批量操作等问题,也需要检查生成的 SQL。ORM 把一部分持久化工作交给了框架,但并没有免去我们理解数据库的责任。
MyBatis 仍然适合什么场景
理解了 ORM 的价值,也不必急着把所有 MyBatis 项目都迁过去。有些需求本来就适合直接围绕 SQL 处理,MyBatis 在这些地方依然很实用。
第一,复杂报表和统计查询。
这类需求关注的是多表关联、聚合、窗口函数以及查询性能。查询结果往往是为某张报表专门组织的,并不对应一个业务聚合。直接用 SQL 表达,再映射成需要的数据结构,通常更清楚。
第二,遗留数据库。
如果数据库已经运行多年,表结构很难调整,又依赖较多存储过程、视图或特定 SQL,使用 MyBatis 往往更容易与现有系统衔接。此时引入 ORM,需要评估映射和改造的成本。
第三,读模型。
在读写职责分离(CQRS)或查询比较复杂的系统中,可以用 JPA/Hibernate 承载写模型,用 MyBatis、jOOQ 或原生 SQL 完成读取。修改订单时需要维护业务规则,展示订单列表时可能只需要几个字段,两者完全可以采用不同的实现方式。
第四,对 SQL 控制要求极高的基础设施型系统。
例如数据同步、批量数据处理等场景,往往需要精确控制 SQL、批次和执行方式。如果主要工作就是读取、转换和写入数据,直接使用 SQL 工具通常更贴近需求。
对于业务规则复杂、同时又有大量查询需求的应用,一个值得考虑的组合是:用 JPA/Hibernate 管理以聚合为中心的写模型,用 SQL 工具处理复杂的读模型。 不必要求同一种工具覆盖所有场景。
结论
从 MyBatis 到 JPA/Hibernate,首先是看待业务代码的角度发生了变化。过去拿到需求,我们可能先想要操作哪些表;开始做领域建模之后,我们会先想有哪些业务概念、它们遵循什么规则、一次操作会带来怎样的状态变化。持久化方案的选择,也就有了不同的依据。
如果系统以简单的数据维护为主,或者复杂度主要在查询和 SQL 控制上,MyBatis 依然是直接、合适的选择。如果业务规则、状态转换和聚合一致性占据了主要精力,我会更倾向于优先考虑 JPA/Hibernate,让持久化机制配合领域模型的工作方式。
当然,换一个框架不会自动得到好的领域模型,用 MyBatis 也不妨碍实践 DDD。真正需要补上的,是如何让对象表达业务、如何划分职责,以及如何让规则有一个明确的归属。理解了这些,再去学习 ORM,就更容易明白它为什么存在,也更容易判断什么时候值得使用它。