ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot双ORM实战:JPA与MyBatis-Plus共存方案与踩坑记录

SpringBoot双ORM实战:JPA与MyBatis-Plus共存方案与踩坑记录 接手过几个用 SpringBoot 做数据访问层的项目之后我越来越觉得现在的 Java 生态在 ORM 这件事上已经被 JPA 和 MyBatis-Plus 两派势力切割得泾渭分明。团队里有人号称“JPA 万能”有人坚持“SQL 在手天下我有”最极端的项目我见过连一个自定义查询都要硬生生用 Specification 怼出来也见过为了一个简单的联表查询写了三百行 XML。后来在一个实际落地的项目中我尝试把 JPA 和 MyBatis-Plus 放进同一个 SpringBoot 工程里让它们各自负责自己最擅长的场景效果出乎意料地稳。这篇文章就把整个双 ORM 的搭建过程、设计取舍、踩坑记录和实测结论完整写出来供还在纠结“到底用哪个”的团队参考。先说清楚这个东西是什么它不是在一个项目里装了 Maven 依赖就算完事而是要在 SpringBoot 的自动配置机制下让 Spring Data JPA 和 MyBatis-Plus 两套持久层框架同时存活、互不干扰、各司其职。它的核心价值在于继承了 JPA 在复杂领域模型、级联关系和状态管理上的优势同时保留了 MyBatis-Plus 在灵活 SQL、动态查询和批量操作上的高效。如果你正在做一个既有复杂业务对象、又有大量统计报表和动态筛选条件的系统这篇文章的完整方案可以直接参考复现。对于数据库访问层还停留在“一个框架打天下”阶段的朋友这篇内容也能帮你理清楚两者之间的边界和底层逻辑。1. 为什么一个项目里要同时保留 JPA 和 MyBatis-Plus 两套 ORM1.1 先认清 JPA 和 MyBatis-Plus 各自的底牌很多人在选型的时候喜欢把 JPA 和 MyBatis-Plus 放在一个维度上对打好像它们是非此即彼的关系。实际上这两套框架的设计哲学差异非常大根本不是一个层面上的东西。JPAJava Persistence API是 Java 官方的持久层规范Spring Data JPA 只是它的一个实现封装。它的核心不是“帮你写 SQL”而是“帮你管理对象状态”。你把实体类往 EntityManager 里一挂后面的新增、更新、删除、级联、延迟加载都是框架通过持久化上下文Persistence Context自己跟踪完成的。换句话说JPA 关心的是“你的对象处于什么状态”而不是“执行了什么 SQL”。MyBatis-Plus 则是 MyBatis 的增强工具它没有去模仿 JPA 的对象状态管理而是保留并且强化了 SQL 的本位主义。你写一个BaseMapper.insert(entity)MP 在背后帮你生成的是实实在在的 INSERT 语句。你想要什么样的 SQL完全可控。MP 更多解决的是“单表 CRUD 不用写 SQL”的繁琐问题本质上是 MyBatis 之上加了一层自动化的半 ORM。理解了这层区别就会明白真正的项目里根本没有必要二选一。复杂的业务聚合、状态流转、关联关系管理交给 JPA 是顺手的事而动态条件查询、批量操作、复杂统计报表用 MyBatis-Plus 写起来才叫痛快。两个一起用以场景为导向做访问层设计才是这篇文章想要达成的目标。1.2 双 ORM 的边界划分避免“两条狗抢一块骨头”双 ORM 最忌讳的事情就是边界不清。如果你在同一个 Repository 里既用 JPA 的findByXxx又用 MyBatis-Plus 的QueryWrapper那个工程迟早变成一团乱麻。我在项目里做过的边界划分可以大致归纳成这样领域模型聚合根的 CRUD、级联关系、事务内状态变更走 JPA。典型的比如订单实体、用户实体、权限模型这类对象有明确的生命周期状态变更频繁JPA 的脏检查机制能省掉大量显式 update 调用。查询报表、动态列表筛选、批量导入导出、复杂联表 SQL走 MyBatis-Plus。这类场景几乎都是只读的返回的是 VO/DTO而不是受管理的实体对象用 SQL 直接投影效率最高。简单的单表字典数据、配置数据两套框架都行但为了统一维护我一般会倾向于 MyBatis-Plus因为写起来最简洁。边界划分完了以后在代码层面也要做物理隔离。JPA 的实体类放在domain/entity包下MyBatis-Plus 的实体类和数据对象放在infrastructure/persistent包下。Service 层通过仓储接口访问数据不直接暴露底层用的什么 ORM这样以后想重构某一块的持久化实现影响范围可以控制在很小的范围内。1.3 哪些团队和项目真正适合双 ORM 方案不是所有项目都适合双 ORM。如果你的项目就是一个简单的 CRUD 后台管理单表操作占 80%两张表的 join 就算复杂查询了那老老实实用 MyBatis-Plus 一个就够了引入 JPA 纯属增加心智负担。反过来如果项目是典型的 DDD 架构业务逻辑严重依赖聚合根和领域事件所有查询都可以由 Specification 或者 QueryDSL 解决那纯 JPA 也完全可以跑通。真正适合双 ORM 的项目通常具备三个特征第一领域模型复杂且存在大量实体关联关系第二报表统计和动态查询的需求密集出现而且 SQL 经常需要手工调优第三团队里有熟悉 JPA 的成员也有擅长写复杂 SQL 的成员。在项目演进过程中我还发现一个额外的收益当业务团队争论某段逻辑应该怎么写的时候可以同时用两套框架给出实现对比性能和可维护性之后再做决定这种灵活性在单 ORM 的项目里是体会不到的。2. 双 ORM 环境搭建与自动配置打通2.1 Maven 依赖组合与版本兼容性考量把 JPA 和 MyBatis-Plus 放进同一个工程第一步就是处理依赖。这一步看着简单实际上坑不少最典型的就是 MyBatis 和 MyBatis-Plus 的版本冲突以及spring-boot-starter-data-jpa和mybatis-plus-boot-starter之间的自动配置干扰。以我目前的稳定组合为例SpringBoot 用的 2.7.x对应的依赖配置如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency这里需要注意的是mybatis-plus-boot-starter里面已经内置了 MyBatis 的依赖所以不需要再单独引入mybatis-spring-boot-starter否则会出现两个 SqlSessionFactory 互相打架的情况。我在第一次搭建的时候踩过这个坑启动日志里出现了两个SqlSessionFactoryBean一个来自 MyBatis 官方 starter一个来自 MP结果 Mapper 扫描的时候出现了随机注册有的接口能用有的接口直接报Invalid bound statement。另外如果你用的是 SpringBoot 3.xJPA 部分没问题但 MyBatis-Plus 需要升级到 3.5.5 以上版本才支持 Jakarta 命名空间千万别拿 3.5.3 的版本往 SpringBoot 3 里面塞ClassNotFound 异常会让你 debug 到怀疑人生。2.2 一个被忽略的问题JPA 和 MP 的实体扫描共存很多教程在讲双 ORM 的时候只讲了“依赖加进去、配置写上、Mapper 和 Repository 都能用”但实际跑起来会卡在一个很隐蔽的地方——实体扫描。Spring Data JPA 默认会扫描Entity注解的类MyBatis-Plus 的TableName注解类则由 MyBatis 的typeAliasesPackage处理。如果两套框架扫描的包范围重叠并且某个实体类上同时加了Entity和TableName本身不会报错但是如果你在 JPA 的application.yml里配置了ddl-auto: updateJPA 会试图按照实体类去创建或者更新表结构而 MyBatis-Plus 这边又按照自己的逻辑去操作同一张表双写环境下很容易出现字段类型对不上的问题。我比较推荐的做法是物理隔离JPA 的实体统一放在xxx.domain.entityMyBatis-Plus 的实体放在xxx.infra.persistent。spring.jpa.properties.javax.persistence.schema-generation.database.action设为noneJPA 完全不做 DDL表结构统一交给 Flyway 管理。这样不但避免了扫描冲突也让整个数据库变更流程变得更可控。2.3 yml 配置的完整下发双数据源还是单数据源双 ORM 不等于双数据源这一点要首先想清楚。如果你的两个 ORM 访问的是同一个数据库完全可以共用一个DataSource、一个PlatformTransactionManager配置上要做的只是让两套框架各取所需。以下是一份亲测可用的配置模板spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root jpa: hibernate: ddl-auto: none show-sql: true open-in-view: false properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml type-aliases-package: com.example.infra.persistent configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这段配置里最有价值的是open-in-view: false它是 JPA 默认开启的 OSIV 模式如果不关掉Controller 层只要拿实体做任意的延迟加载都会抛LazyInitializationException或者悄悄保持数据库连接不释放。生产环境必须关闭 OSIV 并在 Service 层完成所有必要的关联抓取。2.4 两套框架的事务统一策略双 ORM 下的事务管理很多人最担心的是“JPA 的事务和 MyBatis 的事务是不是两套机制”。实际上只要共享同一个DataSourceSpring 的事务抽象会把两个框架的操作纳入同一个数据库事务。关键在于你用的是Transactional注解并且事务管理器是DataSourceTransactionManager。Spring Data JPA 在没有额外配置时会自动创建JpaTransactionManager它和DataSourceTransactionManager并不是同一个对象。如果 MyBatis-Plus 也注册了自己的事务管理器默认情况下 Spring 会选择优先级高的那个最后可能会出现“JPA 提交了MyBatis 还没提交”的诡异现象。我的做法是明确指定一个事务管理器Configuration public class TransactionConfig { Bean Primary public PlatformTransactionManager transactionManager( DataSource dataSource, EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } }JpaTransactionManager本身是基于数据源连接的MyBatis-Plus 的 SqlSession 也能参与到它管理的事务中。实际测试下来在一个 Service 方法里先调用 JPA Repository 保存订单再调用 MyBatis-Plus 的 Mapper 插入订单详情中途抛异常时两者会一起回滚不会出现只回滚一半的情况。3. 双 ORM 核心场景实操从 CRUD 到复杂查询的完整设计3.1 JPA 管领域模型MyBatis-Plus 管查询模型的实体拆分实体拆分是双 ORM 实践里最重要的一步。很多人习惯一张表对应一个实体类这个习惯在双 ORM 环境下要改一改。我建议的做法是这样的对于核心业务表比如订单表你可以有两个类。一个是 JPA 的领域实体OrderEntity带Entity、Table、Id、GeneratedValue注解里面维护订单和订单项的OneToMany关系另一个是 MyBatis-Plus 的持久化对象OrderPO带TableName(t_order)、TableId(type IdType.ASSIGN_ID)注解字段里不维护任何关联关系纯粹就是表结构映射。JPA 实体服务于业务逻辑层MyBatis-Plus 实体服务于查询和报表层。最终在application.yml或代码中设置 JPA 的实体扫描只扫domain.entity包MyBatis-Plus 的type-aliases-package只配infra.persistent包两边的实体互不感知。这样拆分之后领域模型可以从 JPA 的级联和生命周期管理中获益而查询层不会被一些懒加载代理对象绊住手脚。3.2 Repository 层的混合写法让两个 ORM 共存于同一 ServiceRepository 层的设计是整个实践的核心。我的习惯是JPA 侧定义OrderRepository extends JpaRepositoryOrderEntity, Long方法命名遵循 Spring Data 规范比如findByStatusAndCreateTimeBetween。MyBatis-Plus 侧定义OrderMapper extends BaseMapperOrderPO对于复杂的统计 SQL在OrderMapper.xml中手写。Service 层注入时尽量使用面向接口的写法。比如在一个订单查询服务里Service public class OrderQueryService { private final OrderRepository orderRepository; private final OrderMapper orderMapper; public OrderQueryService(OrderRepository orderRepository, OrderMapper orderMapper) { this.orderRepository orderRepository; this.orderMapper orderMapper; } public PageOrderVO pageQuery(OrderPageQuery query) { PageOrderPO page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperOrderPO wrapper Wrappers.lambdaQuery(OrderPO.class) .eq(StringUtils.hasText(query.getOrderNo()), OrderPO::getOrderNo, query.getOrderNo()) .eq(query.getStatus() ! null, OrderPO::getStatus, query.getStatus()) .between(query.getStartTime() ! null query.getEndTime() ! null, OrderPO::getCreateTime, query.getStartTime(), query.getEndTime()) .orderByDesc(OrderPO::getCreateTime); return orderMapper.selectPage(page, wrapper); } Transactional(readOnly true) public OrderDetailVO detail(Long orderId) { OrderEntity orderEntity orderRepository.findById(orderId) .orElseThrow(() - new BizException(订单不存在)); ListOrderItemEntity items orderEntity.getItems(); OrderDetailVO detail new OrderDetailVO(); detail.setId(orderEntity.getId()); detail.setItems(items.stream().map(OrderItemVO::from).toList()); return detail; } }这个例子清晰地展示了双 ORM 的协作方式。列表分页查询这种动态 SQL 密集的场景用 MyBatis-Plus 的 LambdaQueryWrapper 写起来非常自然不会有 JPA Specification 那种令人窒息的“又臭又长”。而订单详情这种需要从根实体出发抓取子集合的场景用 JPA 的实体关联就顺手多了延迟加载加Transactional保证会话内完成抓取逻辑简单明了。3.3 分页查询的差异与统一返回值适配分页是双 ORM 最容易踩坑的地方因为两套框架对页码的起始定义不一致。JPA 的PageRequest.of(page, size)中页码从 0 开始前端传的第一页在 JPA 里是page 0MyBatis-Plus 的Page构造器中current从 1 开始。如果不做统一转换一旦同一个前端接口在切换底层 ORM 后返回的数据会错位。我在项目中封装了一个PageResultT作为统一返回体同时在入口处把所有外部传入的页码统一减一再交给 JPAMyBatis-Plus 则直接使用原始页码。这样前端对接根本不需要关心后端用的是哪套框架。同时MyBatis-Plus 的分页需要引入分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } }这个插件也负责 SQL 解析优化如果没加它selectPage查出来的数据会是全量列表再内存分页数据量一大性能直接崩。加上maxLimit是为了防止有人恶意传一个超大页码把数据库打爆。3.4 复杂统计报表用 MyBatis-Plus 原生 SQL拒绝对象关系绑架查询报表是双 ORM 方案最受益的场景。用 JPA 做多表聚合统计要么写 JPQL要么用 Specification 动态拼接复杂度高且 SQL 不可控。换成 MyBatis-Plus 之后在 Mapper 接口里直接声明方法在 XML 里写原生的灵活 SQL性能与维护性兼顾。举个例子统计各渠道的订单量和销售额select idselectOrderStats resultTypecom.example.infra.persistent.OrderStatsPO SELECT channel_code AS channelCode, COUNT(*) AS orderCount, SUM(order_amount) AS totalAmount FROM t_order WHERE order_status 2 AND create_time BETWEEN #{startTime} AND #{endTime} GROUP BY channel_code /selectMapper 侧public interface OrderStatsMapper extends BaseMapperOrderStatsPO { ListOrderStatsPO selectOrderStats(Param(startTime) LocalDateTime startTime, Param(endTime) LocalDateTime endTime); }这段 SQL 放在 JPA 里写就非常尴尬SUM、GROUP BY的投影结果不是一个实体类JPA 需要定义接口投影或者用Object[]写出来的代码感觉像是在给框架打工。而在 MyBatis-Plus 里这是稀松平常的操作结果直接映射进 POJO 即可。4. 双 ORM 的状态管理差异及主键生成策略统一4.1 JPA 的脏检查机制和 MP 的字段更新策略到底区别在哪JPA 的更新逻辑是“对象状态驱动”。你在事务里findById拿到实体修改几个字段事务提交时 Hibernate 会执行快照比对把发生变化的地方生成 UPDATE 语句。这个机制的好处是代码里不需要写 update 方法坏处是对刚入门的人不友好经常出现你以为没保存结果 Hibernate 莫名其妙更新了一堆字段。MyBatis-Plus 的更新逻辑是“条件驱动”。updateById(entity)默认会忽略实体中为 null 的字段也就是说你只想更新某个字段其他字段只要设成 null就不会出现在 UPDATE SET 语句里。这个特性在“部分更新”的场景下非常实用但也有一个经典坑当业务要求把数据库某字段更新成 NULL 时updateById默认不生效必须显式使用UpdateWrapper的set(column, null)。在实际双 ORM 项目中我的做法是JPA 实体只负责领域状态流转业务上完整更新用saveMP 负责持久化层的灵活字段更新局部更新一律用LambdaUpdateWrapper绝不依赖实体里那个不太确定的 null 忽略规则。这样每个 ORM 的“怪异点”都被规避了只保留它们的优势面。4.2 主键策略冲突JPA 的 IDENTITY 与 MP 的 ASSIGN_ID在主键策略上JPA 和 MyBatis-Plus 的默认理念完全不同。JPA 的GeneratedValue(strategy GenerationType.IDENTITY)依赖数据库自增MyBatis-Plus 自带IdType.ASSIGN_ID通过雪花算法在应用层生成 19 位分布式 ID。如果双 ORM 共用同一张表主键生成策略必须要统一否则一边插入依赖数据库自增一边应用层预生成 ID日志里就会出现令人迷惑的“主键重复”或“主键为 0”的报错。我的推荐是统一使用 MyBatis-Plus 的雪花 ID。核心原因是分布式环境下数据库自增 ID 有非常多的风险比如合并分库分表时冲突、批量插入性能差、ID 可预测导致信息泄露。统一方案如下JPA 实体上不再使用GeneratedValue改为主键字段手动设置值或者使用自定义的IdentifierGenerator。所有实体的主键赋值入口统一放在一个“领域对象工厂”或者 Service 的创建方法中插入前调用IdWorker.getId()生成主键。MyBatis-Plus 侧配置id-type: assign_id如果是 MP 的insert则会自动填充。实际操作时由于 MyBatis-Plus 的insert会判断主键是否为空为空才生成 ID所以保存时传入IdWorker.getId()并不会冲突。4.3 逻辑删除在双 ORM 框架中的一致性处理MyBatis-Plus 的逻辑删除支持非常成熟配置好logic-delete-field后所有 MP 的查询会自动追加deleted 0条件更新也会自动带上。但 JPA 没有这个能力Hibernate 只认物理删除或者依赖SQLDelete和Where这种 Hibernate 特有的注解。在双 ORM 项目中如果同一张表分别被 JPA 和 MyBatis-Plus 操作逻辑删除的配置必须统一。我的方案是业务核心表被 JPA 管理的领域实体使用SQLDelete和Where注解让 Hibernate 层面的删除也变成逻辑删除查询自动过滤已删除数据。查询统计类表主要由 MP 管理使用 MP 的逻辑删除配置。建表统一包含deleted字段类型为tinyint(1)默认0。这里有一个非常值得注意的细节当同一个实体同时挂SQLDelete和 MP 的TableLogic时JPA 侧的删除和 MP 侧的删除都会生效。但如果你在 JPA 实体里配置了Where而 MyBatis-Plus 查询时又额外使用selectList此时 SQL 里会出现两个deleted 0条件虽然结果没问题但看着很不舒服排查问题时容易造成误解。所以强烈建议在代码评审阶段确定每张表的主从 ORM 归属避免双框架在同一张表上反复横跳。5. 双 ORM 项目中的常见问题与排查实录5.1 启动时两个 SqlSessionFactory 互相干扰这个问题在搭建初期非常典型而且报错信息极其暧昧。常见表现是应用启动正常但调用某个 Mapper 方法时报Invalid bound statement (not found)然而你明明已经在 XML 里写了对应的 statementId。排查步骤是这样的看启动日志中打印的SqlSessionFactory实例数量如果出现两个基本就是依赖冲突。检查依赖树中是否同时存在mybatis-spring-boot-starter和mybatis-plus-boot-starter有的话排除官方 starter。确认spring.main.allow-bean-definition-overriding的配置如果某个类被两个自动配置重复注册这项配置可能掩盖了冲突。我在项目里通过mvn dependency:tree排除了mybatis-spring-boot-starter之后重启应用问题立刻消失。后面只要有人新加依赖导致启动异常第一件事就是查依赖树不要凭直觉去改 XML 或 Mapper 注解。5.2 JPA 懒加载异常在双 ORM 项目里的表现尤为突出LazyInitializationException是 JPA 开发者的老熟人但在双 ORM 架构里它出现的场景会更多。因为业务层可能同时调用 JPA Repository 和 MP Mapper如果某一个 Service 方法没有加TransactionalJPA 实体从数据库查出来之后 Session 立即关闭再访问getItems()就炸了。我踩过的最隐蔽的一次是在事务方法里把 JPA 实体返回给了 MP 的查询结果封装类外层 JSON 序列化时访问了实体的懒加载属性异常信息被 Jackson 的序列化包装后显得乱七八糟看不懂到底是哪里出的问题。后来定了三条铁律JPA 实体的懒加载属性的访问必须在Transactional方法内完成。任何实体出 Service 层之前必须转换成 DTO/VO。禁止在 Controller 层直接返回 JPA 实体对象。这三条规矩执行之后懒加载问题基本上消失了代价是多写几个convert方法但可维护性提升非常明显。5.3 批量插入的性能陷阱JPA 的 saveAll 与 MP 的批量能力对比双 ORM 项目里批量插入通常会有两种做法性能差异巨大。JPA 的saveAll本质上是一条一条地 insert一万条数据要往返一万次数据库MyBatis-Plus 的insertBatchSomeColumn则能把数据拼成一条多 VALUES 的 INSERT性能差距可能在数量级以上。这算是我强烈建议把批量导入类操作统一交给 MyBatis-Plus 的原因之一。使用 MP 做批量插入时需要注意 MySQL 对 SQL 长度有限制默认max_allowed_packet是 4MB一万条带很多字段的 INSERT 很容易超限。我的经验是每批控制在 500 到 1000 条同时设置明确的批大小参数既不会太慢也不会超包体。如果你的项目没有使用insertBatchSomeColumn这种扩展方法可以自己实现一个 SQL 拼接工具把插入语句拼成 insert into ... values (),(),() 的形式效果一样。5.4 日志打印混乱show-sql 与 mp 的 log-impl 同时开启JPA 和 MyBatis-Plus 各自有日志输出渠道。spring.jpa.show-sqltrue会通过 Hibernate 打印 SQLMP 的log-impl: StdOutImpl也会打印 SQL。两者同时开启时同一个请求会出现两套风格完全不同的日志排查问题时容易让人看花眼。我的建议是开发环境只保留 MP 的日志JPA 的show-sql关闭。Hibernate 的 SQL 日志格式偏底层参数占位符还要自己对着输出猜调试体验远不如 MP 的预编译参数直出方便。需要分析 Hibernate 生成的 SQL 时再临时开show-sql配合format_sqltrue使用。5.5 实体类同时加两套注解的维护成本与注意事项在双 ORM 实践中很多团队尝试用一套实体类同时承载Entity和TableName这样确实能减少类数量但维护成本会显著上升。原因在于两套框架对字段的默认命名规则不同、主键策略不同、乐观锁字段支持不同揉在一起之后类上会被各种注解占满后续任何改表结构的操作都要同时考虑两套框架是否兼容。如果你坚持要共用一套实体类有几个检查点务必盯住主键字段必须同时有Id或GeneratedValue与TableId。逻辑删除字段必须同时适配 Hibernate 的SQLDelete和 MP 的TableLogic。无参构造器必须存在且不要定义为 private。避免在实体类里使用final字段Hibernate 代理和 MyBatis 的反射都会受影响。不过从我个人的角度讲只要项目不是特别小还是坚决推荐拆分成两套类。类数量多一点没关系职责清晰比一切都重要。6. 双 ORM 的实际应用效果与进一步优化6.1 一个订单服务的完整双 ORM 调用链路复盘前面讲了不少概念和配置这里把整个订单服务的一个业务链路串联起来看双 ORM 在真实运行中是怎么协作的。场景是“用户下单并查询自己的历史订单”。流程如下Controller 层接收请求调用OrderApplicationService#placeOrder。Service 方法标注Transactional先通过OrderRepositoryJPA创建订单根实体设置订单状态、关联用户信息。同一个事务内调用OrderItemRepository.saveAll保存订单项集合JPA 负责对象关系的一致性。事务提交前如果需要记录订单快照用于后续统计分析再调用OrderSnapshotMapper.insertMP把订单快照插入到分析表。事务提交缓存失效。用户查询历史订单时Controller 调用OrderQueryService#pageQuery该方法内部走OrderMapper.selectPage通过 LambdaQueryWrapper 拼接动态过滤条件返回分页 VO。整个链路里JPA 负责了状态管理和对象关系MP 负责了查询落地和批量写入。每个环节都工作在各自最擅长的位置不会出现在事务内手动拼接 SQL 的尴尬局面也不会有 JPA 硬写统计报表的痛苦。6.2 从双 ORM 到多源数据访问的扩展思路如果你在双 ORM 上跑通了后面需求演进到多数据源扩展方向是非常顺滑的。比如项目中需要同时操作 MySQL 和 PostgreSQL或者需要引入一个只读的分析库基于双 ORM 的经验可以把数据源也一并隔离主库用 JPA MyBatis-Plus 双活分析库单独使用 MyBatis-Plus配置在两套SqlSessionFactory中指向不同数据源即可。用DS注解或者自定义DynamicDataSourceRouter来切换数据源基本可以做到对业务代码无感ORM 侧的改动也仅限于 Mapper 的SqlSessionTemplate绑定。不过需要提醒的是多数据源环境下事务管理器一定要按数据源分开配置不要把跨库的事务挂在同一个JpaTransactionManager上不然回滚策略基本没法做。6.3 双 ORM 的性能调优与监控建议双 ORM 对比单 ORM最容易被键盘侠攻击的点是“多了一个框架性能是不是变差了”。从我实际压测的结果来看只要正确配置了分页插件、关闭了无用的日志、合理使用了批量插入两个 ORM 各自执行 SQL 的性能差异基本可以忽略。真正影响性能的是对 JPA 的不当使用。比如N1 查询问题。用 JPA 遍历订单然后逐条访问订单项会产生大量 SQL。过度使用懒加载。把懒加载属性放到事务外面访问不但抛异常还可能在低版本 Hibernate 下触发强制抓取导致性能爆炸。不合理的ddl-auto: update。生产环境开启这个配置每一次启动都会做表结构比对大表多的时候启动时间会显著变长。建议接入DataSource层面的 SQL 监控中间件比如 druid 的监控页面或者 p6spy统一打印所有经过数据源的 SQL无论哪套 ORM 生成的都能够看到。这也是排查双 ORM 问题最快的手段。6.4 结合通用 CRUD 模式进一步降低重复代码拥有双 ORM 能力之后我最后做了一件事把两套框架都包了一层通用的 CRUD 模式。JPA 侧封装了BaseDomainServiceT提供保存、删除、按 ID 查找方法MyBatis-Plus 侧封装了BaseDataServiceT提供分页查询、批量插入方法Service 层代码量肉眼可见地减少同时保持了方法命名的业务化。要注意的是这类“没有状态的通用 CRUD 服务”只是工具类不要试图把它变成业务对象。如果你的代码里出现baseMapper.selectPage直接写在 Controller 层的情况那不管用什么 ORM 框架抽象层都已经形同虚设了。6.5 部署与运维视角如何保证双 ORM 项目的平稳发布双 ORM 项目部署层面没有特殊负担它本质还是一个 SpringBoot 应用。需要注意的是因为引入了 JPA实体类映射关系在启动阶段会做校验如果表结构缺失或者字段不匹配应用启动会失败。这一点相比 MyBatis-Plus 启动时不校验表结构而言上线要求更严格但反过来也是一次提前暴露问题的机会。我的做法是在 CI 流程中加一步启动健康检查打包完成之后先在临时环境启动应用等待ApplicationReadyEvent事件触发再执行一组冒烟测试 SQL。一旦 JPA 元模型校验失败构建直接失败根本不会走人工测试环节。这套流程跑通后表结构变更引发的线上事故基本清零。7. 常见问题速查与双 ORM 选型建议7.1 高频问题排查速查表整理一份我在双 ORM 实践里高频遇到的问题以及对应的解法方便有类似情况的朋友直接对照排查。现象可能原因解法启动出现两个 SqlSessionFactory同时引入 mybatis starter 和 mp starter排除 mybatis-spring-boot-starterMapper 报 Invalid bound statementMapper XML 没被扫描到检查 mapper-locations 路径清 Maven 缓存JPA 实体查询返回代理对象无法 JSON 序列化懒加载属性在事务外访问转 DTO/VO禁止直接返回实体MP 批量插入报 PacketTooBig单批数据量过大分批插入每批 500~1000 条JPA 控制事务MP 操作未回滚事务管理器冲突统一配置 JpaTransactionManager分页数据错位JPA/PMP 页码从 0/1 不一致封装统一分页结果入口转换页码逻辑删除查询结果不一致双框架逻辑删除配置不一致按主从 ORM 原则配置单一边统一deleted字段生产启动很慢开启 ddl-auto: update改 none用 Flyway 管理 DDL这张表覆盖了我遇到的 90% 以上的问题剩下 10% 基本是代码自身的逻辑错误。7.2 到底什么时候应该选择双 ORM什么时候该果断放弃写到最后我觉得有必要回到根本问题双 ORM 是不是必须的。如果你所在的团队很小或者项目规模不大我个人建议不要轻易尝试双 ORM。JPA 本身就有学习曲线MyBatis-Plus 再叠加进来对新人而言就是一堵墙。与其花时间维护两套框架不如在单 ORM 上做到极致。但如果你的项目已经出现了以下信号认真评估双 ORM 的收益是值得的领域模型复杂度高同时存在大量动态报表查询。开发团队规模中等有人熟悉 JPA 对业务建模有帮助也有人擅长 SQL 优化。项目正在从传统的“表驱动”开发模式向 DDD 模式演进短期内无法完成全部模型的改造。双 ORM 不是什么银弹它更像是一种“组合拳”。JPA 负责守住对象模型的领地MyBatis-Plus 负责开疆拓土处理 SQL 的脏活累活两者之间的边界只要清晰维护成本并不会比单 ORM 高到哪去。反过来如果边界的决策摇摆不定那这套架构只会加速代码腐化。根据我个人的体会双 ORM 实践中最重要的一点不是技术能力而是“克制”。知道什么时候该用哪个框架比熟练掌握某个框架要难得多。当一段查询逻辑可以被 JPA 写成 5 行、也可以被 MyBatis-Plus 写成 8 行的时候优先考虑团队的熟悉度和后续维护场景而不是短期写起来更爽的那一个。另外在项目实际迭代过程中我还有一个非常真实的感受一旦把“双 ORM”当作既定的架构事实而不是技术选型团队反而会开始认真讨论每个查询到底是什么类型、需要什么结果、应该从哪里取数据。这比任何代码规范都更能提升访问层的质量。这可能就是双 ORM 架构带给我最大的额外收获它倒逼你去理解数据访问的本质。
返回列表