ARTICLE DETAIL

资讯详情

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

MyBatis vs JPA vs 混合架构:企业ORM选型实战指南

MyBatis vs JPA vs 混合架构:企业ORM选型实战指南 “第六课 · 6.4 企业真实选型MyBatis / JPA / 混合架构”——这个标题一看就是过来人写的。网上聊 MyBatis 和 JPA 的文章十个有八个在站队吵得不可开交但真到了企业项目里你会发现大多数团队根本没这么多讲究老项目可能从 iBatis 时代就扎在 XML 里新团队可能上来就 Spring Data JPA 一把梭还有不少中大型系统表层查询用 JPA、复杂统计和报表硬啃 XML两边各管一摊。这个课讲的正是最后一种“混合架构”而我的实际经验是选型从来不是选最好的框架而是选团队接得住、项目活得下去的框架。这篇文章我就从企业真实选型的角度把 MyBatis、JPA、混合架构这三条路全部拆开来讲包括底层原理、适用场景、混用时的边界划分、事务和缓存这些容易翻车的细节最后用同一个订单查询需求把三种写法的代码都摆出来给你看。无论你是刚接触 ORM 的新人还是正在带团队做技术选型的负责人这篇都值得你花十几分钟认真读完。1. 先别急着站队选型背后的三条底层逻辑1.1 团队写代码的方式决定框架的“语言”选 ORM 框架表面上看是技术选型实际上是选一种团队协作的“语言”。我见过一个很典型的案例一个外包团队成员流动率很高三个月换一拨人项目用的是 JPA结果每次新人进来都要花两周搞懂实体关系映射、懒加载、级联操作这些概念出 bug 的概率还不低。后来换成了 MyBatis新来的 Java 开发只要会写 SQL看一眼 XML 就能上手改需求交接成本直线下降。这不是说 JPA 不好而是说JPA 的抽象层要求团队成员对 ORM 有稳定的认知水平否则“自动”生成 SQL 的特性就会变成黑盒出了问题你连排查方向都没有。而 MyBatis 是“半自动”的SQL 你自己写映射关系你自己配运行机制清晰透明心智负担天然低一截。所以选型第一件事是问自己你的团队是什么水平人员流动大不大能不能维持稳定的 JPA 实践规范1.2 项目活下去的周期决定框架的“野心”第二件事是看项目的生命周期。我做过的项目中有活了三年的内部运营系统也有活过十年的核心交易链路。生命周期不同对框架的要求完全不同。内部管理系统、后台 CRUD 为主的场景表结构通常跟着页面走需求变化快这时候 JPA 的生产力优势非常明显实体定义好Repository 接口一写CRUD 全自动生成很少有手写 SQL 的冲动。而交易、账务、报表这类“对 SQL 有执念”的系统表结构经过严谨设计查询路径很长优化空间很大你不可能让 JPA 替你决定 SQL 怎么写这时候 MyBatis 的手写 SQL 反而成了核心优势。一句话总结短期项目求快JPA 合适长期系统求稳MyBatis 更可控。但真实企业里哪有那么非黑即白所以就有了第三种答案——混合架构。1.3 生态和周边才是真正卡脖子的地方框架本身的 CRUD 能力只是冰山一角真正影响开发效率的是生态。MyBatis 有 PageHelper、MyBatis-Plus、MyBatis Generator还有各类代码生成插件周边工具极其丰富。JPA 背靠 Spring Data 大族Specification、Auditing、数据校验全套集成配合 Spring Boot 几乎是零配置启动。在选型调研时我强烈建议你花时间查一下这个框架在你项目依赖的中间件下表现如何比如你用了国产数据库 GaussDBMyBatis 对 GaussDB 的支持就比较友好毕竟 SQL 方言是手写的数据库换了你改 SQL 就行而 JPA 的方言Dialect机制要额外适配Hibernate 对非主流数据库的支持往往滞后。这种“周边适配”问题在选型阶段几乎不会有人提但上线后踩到一次就是大坑。2. MyBatis把 SQL 攥在自己手里的快乐与代价2.1 为什么那么多团队离不开 MyBatisSQL 可控性是真的爽MyBatis 的核心设计其实一句话就能讲完它不替你生成 SQL只帮你把 SQL 和参数、结果集映射起来。它的每一次查询过程大概是解析 XML 或注解里的 SQL 语句字符串用动态 SQL 拼接成最终可执行语句然后通过 JDBC 执行再把 ResultSet 按 ResultMap 或自动映射规则转成实体对象。这中间你看得见摸得着的就是自己的 SQL 语法、自己的映射规则出问题查日志就完了不涉及任何黑盒。这一点对老手来说太重要了。MySQL 的 EXPLAIN 分析、慢 SQL 优化、Oracle 的大字段处理、GaussDB 的方言差异这些操作的前提都是你能完全控制 SQL 文本。比如一个多表关联的报表查询MyBatis 里你可以自由决定 JOIN 顺序、临时表、子查询、UNION 结构把执行计划调优到极致。而 JPA 想做到同样的事要么写 Query 原生 SQL等于放弃 JPA 优势要么一点点调 Specification 的谓词让 Hibernate 生成合适的 SQL绕来绕去效率极低。2.2 动态 SQL 和 TypeHandlerMyBatis 两个最能打的特性MyBatis 最被低估的两个能力我实际用下来觉得比很多人想象中重要得多。第一个是动态 SQL。以前用 JDBC 拼 SQL最恶心的就是各种判空拼接一个查询分支一多字符串拼接代码比业务逻辑还长。MyBatis 的if、where、foreach、choose标签把这种拼接从 Java 里解放出来XML 里看着复杂但写起来其实很顺手。比如一个订单列表查询状态、时间、商户 ID 都是可选条件用where自动处理 AND 关键字代码瞬间干净很多select idpageQuery resultTypeOrderVO SELECT o.id, o.order_no, o.merchant_id, o.amount, o.status, o.created_at FROM t_order o where if testmerchantId ! null AND o.merchant_id #{merchantId} /if if teststatus ! null AND o.status #{status} /if if teststartTime ! null AND o.created_at gt; #{startTime} /if if testendTime ! null AND o.created_at lt; #{endTime} /if /where ORDER BY o.created_at DESC LIMIT #{offset}, #{pageSize} /select第二个是TypeHandler。Java 类型和 JDBC 类型之间的转换规则TypeHandler 是花式操作的入口。比如枚举类型你可以自定义一个 TypeHandler把枚举转成存库的 code查出来再转回枚举业务代码完全不用感知存储层的转换。再比如 JSON 字段一个 JacksonTypeHandler 就可以让实体类直接持有 Map 或自定义 POJO 属性省掉手写序列化反序列化的一堆代码。TypeHandler 的本质是“映射规则可编程化”这一点在有特殊类型需求的业务里简直是救命的。顺便说一下工作流程MyBatis 初始化时会在 TypeHandlerRegistry 里注册各种类型处理器执行 SQL 时PreparedStatement 的 setXxx 和 ResultSet 的 getXxx 都会走到 TypeHandler。也就是说参数从 Java 传到数据库是一条链数据库返回的记录变成 Java 对象又是一条链TypeHandler 设在两个链路的中间自定义类型的高低字段、日期格式、枚举映射都可以在这里统一解决。2.3 代价在哪里CRUD 冗余、字段同步、分包管理当然MyBatis 不是银弹三个痛点我在项目里都完整地踩过。第一CRUD 代码量确实多。一个简单的单表增删改查JPA 一个 Repository 接口就搞定了MyBatis 要写 Mapper 接口、XML 映射、ResultMap 三件套如果项目里全是这种简单操作纯 MyBatis 会让开发速度肉眼可见地降下来。这就是为什么后来 MyBatis-Plus 那么火——它就是在 MyBatis 之上补了“自动 CRUD”的功能本质是对“MyBatis 缺通用 CRUD”这个痛点的市场回应。第二字段映射要手动维护。实体类加了一个字段XML 里的 SQL 要同步加ResultMap 也要同步加漏一个就是线上查询字段为空。更麻烦的是重构改表名所有相关 XML 都要排查一遍。我见过一个老项目一张表改名字开发改了 14 个 XML 文件才改完中间还有两个漏改的导致测试期才发现。工具比如 MyBatis Generator能生成基础映射但业务查询里的字段依然得人工维护。第三分包管理容易乱。MyBatis 的 Mapper XML 文件如果没规划好放置路径会散落在各个模块排查问题时要全局搜 SQL。我记得团队里有个约定是 XML 路径与 Mapper 接口包路径完全对齐按模块分目录能在很大程度上缓解这个问题但这也依赖团队纪律。3. JPA / Hibernate领域模型的理想国但天花板很低3.1 JPA 的核心是对象关系映射不是省 SQL很多人误解 JPA 说“JPA 就是帮你自动生成 SQL”这个理解太浅了。JPA 的本质是让你以“对象”为核心建模把表之间的关系一对一、一对多、多对多映射成对象之间的关联关系。你定义一个 Order 实体里面直接有个SetOrderItem items属性查询订单的时候Hibernate 会在背后帮你 join 两张表并把子集合填进去或者按需懒加载。这种“以对象为核心”的思维在业务逻辑复杂的领域模型系统里很有价值你的代码里不再有一堆 DTO 和 VO 的转换代码实体即对象对象即业务。比如做库存系统Stock 和 StockFlow 之间的关系写在实体里业务代码调用stock.getFlows()就能拿到流水而不关心这背后是两张表 JOIN 还是两次查询。这也是 DDD领域驱动设计落地时大家更倾向选 JPA 的原因。3.2 缓存的诱惑与陷阱一级缓存、二级缓存、N1 查询JPA 的缓存机制很容易让新人一脸懵。Hibernate 的一级缓存默认开启范围是 Session也就是一次事务同一个 Session 中查询同一个对象第二次不会打数据库。二级缓存默认关闭需要配置实现如 Ehcache、Caffeine跨 Session 或跨事务共享。理解了这个机制你就明白为什么 JPA 有时查数据“很慢”、有时“很快”其实都是缓存的作用。最大的坑是N1 查询。举个例子查询订单列表每条订单要关联商户名称如果实体关系映射是ManyToOne(fetch FetchType.EAGER)Hibernate 会先查一次订单表然后逐条查询商户表。订单有 100 条等于 1 100 次查询数据库连接池直接被打爆。解决办法是使用JOIN FETCH、EntityGraph或Specification里显式 join把 101 次查询压成 1 次。Query(SELECT o FROM Order o JOIN FETCH o.merchant WHERE o.status :status) ListOrder findByStatusWithMerchant(Param(status) Integer status);很多 JPA 项目性能不好不是 JPA 不行而是团队根本不了解实体关联的加载时机EAGER 和 LAZY 完全凭感觉配结果可想而知。这一点从网上搜“jpa findone 用法”“spring data jpa 和 mybatis-plus 的区别”的热度就能看出来——大量开发者在 JPA 的关联查询和接口命名上卡壳。3.3 什么项目真正适合 JPA让我直说根据我这些年观察下来的经验JPA 真正发挥价值的项目有几个共同特征CRUD 占比很高、表结构相对规范和稳定、团队对 ORM 有统一认知、项目周期在 1-2 年内的业务系统。比如权限管理系统、通知公告系统、简单的 CMS、工单系统这些项目里 80% 的操作是单表或浅关联 CRUDJPA 的 Repository 接口一行代码不写就能跑开发效率是实打实的。反过来凡是查询条件多变、统计报表复杂、SQL 优化需求大的模块JPA 都显得别扭。我之前做过一个渠道对账模块一张核心表 40 多个字段查询条件十几个组合查询的可能分支几十种列要动态控制这种需求你用 JPA 的 Specification 去拼谓词代码量比 MyBatis XML 大得多而且生成的 SQL 还可能关联多余的表。最后我把这个模块直接用原生 SQL 写在 Query 里等于 JPA 外壳 MyBatis 灵魂这其实已经属于“混合架构”的思路了。4. 混合架构在理想和现实之间走钢丝4.1 混合不是乱炖物理隔离还是逻辑共存所谓“混合架构”在真实项目里绝对不是“一个项目里 MyBatis 和 JPA 的代码随机穿插”而是有一个清晰的边界划分。我见过两种落地方式。物理隔离式按业务模块拆分比如订单、支付这些核心交易模块用 MyBatis用户、权限、菜单这些基础管理模块用 JPA。每个模块内只用一种 ORM依赖清晰团队内可以按模块分配技术栈。这种划分方式的优点是互不干扰缺点是如果表之间有跨模块关联查询需要走服务接口或写原生 SQL不能直接连表查。逻辑共存式同一个业务模块里CRUD 走 JPA复杂查询走 MyBatis。比如订单模块的创建、状态流转用 JPA 的实体和 Repository 管订单列表的分页多条件查询用 MyBatis 的 XML 写。这种方式最灵活但要求团队对两种技术栈都有把控能力否则一个事务里既有 JPA 操作又有 MyBatis 操作出了问题定位时要同时懂两套机制的运行原理。我在实际项目里更推荐逻辑共存式但有一个硬性前提每个实体类的数据访问只走一条路。比如 Order 实体删除、更新、主键查询走 JPA列表和统计查询走 MyBatis两边都用的话你会遇到两个框架各自的缓存机制叠加产生的数据一致性问题排查起来极其痛苦。4.2 事务边界怎么统一本地事务各管各的坑混合架构最典型的翻车现场是事务问题。你的 Service 方法加了Transactional但 MyBatis 的 SqlSession 和 JPA 的 EntityManager 在 Spring 里是两个资源Spring 事务管理器同时管这两个资源时用的是JtaTransactionManager 或 ChainedTransactionManager或叫 DataSourceTransactionManager 的“最佳努力” 1PC 模式。如果底层是同一个数据源Spring 会共享事务如果分库那就是跨数据源的事务本地事务管理器根本管不住想保证强一致必须引入分布式事务方案比如 Seata、事务消息。这个坑很多团队是踩到线上才意识到的。我遇到过的情况是Service 方法里先 MyBatis 插入一条记录再 JPA 更新另一条记录两条操作指向同一个 MySQL 数据源Spring 默认用DataSourceTransactionManager只会把 MyBatis 的 Connection 纳入事务JPA 那边如果没显式加入同一个事务管理器就会出现“一个成功一个失败”的诡异现象而且日志里查不出任何异常。排查到最后发现需要注入PlatformTransactionManager并确保两个框架使用同一个 DataSource。我的建议是混合架构下能用声明式事务就不要手动开启事务一个事务方法里尽量不要同时操作两个框架的多个数据源如果必须跨库直接接受最终一致性不要硬上强一致。这是架构层面的权衡不是代码层面的问题。4.3 一个真实的混合架构切片订单系统的读写分离为了让你有体感我拿一个真实的订单系统示例来讲混合架构怎么布局。这个系统假设有三张核心表t_order订单主表、t_order_item订单明细表、t_merchant商户表。日常操作包括创建订单插入主表和明细表事务性强—— 适合 JPA因为实体关系清晰级联保存方便按订单号查询详情包括明细和商户信息—— 适合 JPAfindById 懒加载即可一次事务内访问订单列表分页查询多条件组合下单时间范围、状态、商户 ID、金额范围—— 适合 MyBatis动态 SQL 灵活拼接月度销售统计报表GROUP BY 商户、日期、状态聚合运算多—— 适合 MyBatis手写 SQL 优化方便在架构上我会这样划分操作类型使用框架理由创建/更新/删除订单、订单明细JPA实体关联清晰事务性操作方便订单主键查询、简单条件查询JPARepository 内置方法足够订单多条件分页列表MyBatis动态 SQL 处理可选条件最舒服销售统计、报表类聚合MyBatisSQL 可自由优化Explain 调优这个划分里有一个关键原则订单实体的状态变更和查询都在同一个事务上下文里走 JPA当订单被创建完之后后续的统计查询走 MyBatis 是只读操作不会影响 JPA 的缓存一致性。这样两套框架在同一个数据源下工作配合良好互不干扰。5. 实操对照同一个查询需求三种写法的完整实现5.1 场景设定订单分页查询 商户关联光说不练假把式。接下来我设定一个具体的需求订单列表分页查询入参包括商户 ID可空、订单状态可空、下单时间范围可空、分页页码和页大小返回内容包括订单号、金额、状态、商户名称、下单时间。这个需求是后台系统最常见的列表查询也是最能体现框架差异的场景。我们分别用 JPA、MyBatis、混合方式来实现它代码我都给完整你可以直接对照参考。5.2 用 JPA 的 Specification 实现动态查询JPA 里动态查询有几种方式接口命名方法、Query 注解、Specification。条件多且可选时用 Specification 最灵活。但代码写起来其实不短为了可读性我做了拆分public PageOrderVO pageQuery(OrderQuery query) { SpecificationOrder spec (root, criteriaQuery, builder) - { ListPredicate predicates new ArrayList(); // 商户 ID 可选 if (query.getMerchantId() ! null) { predicates.add(builder.equal(root.get(merchant).get(id), query.getMerchantId())); } // 订单状态可选 if (query.getStatus() ! null) { predicates.add(builder.equal(root.get(status), query.getStatus())); } // 下单时间范围 if (query.getStartTime() ! null) { predicates.add(builder.greaterThanOrEqualTo(root.get(createdAt), query.getStartTime())); } if (query.getEndTime() ! null) { predicates.add(builder.lessThanOrEqualTo(root.get(createdAt), query.getEndTime())); } return builder.and(predicates.toArray(new Predicate[0])); }; PageOrder orderPage orderRepository.findAll(spec, PageRequest.of(query.getPage(), query.getPageSize(), Sort.by(Sort.Direction.DESC, createdAt))); // 转换为 VO注意这里会触发 N1需要在 Specification 或 EntityGraph 里预抓取商户 return orderPage.map(order - { OrderVO vo new OrderVO(); vo.setOrderNo(order.getOrderNo()); vo.setAmount(order.getAmount()); vo.setStatus(order.getStatus()); vo.setMerchantName(order.getMerchant().getName()); // 懒加载 vo.setCreatedAt(order.getCreatedAt()); return vo; }); }注意我代码里的注释order.getMerchant().getName()如果 merchant 是 LAZY 加载每行订单都会额外执行一次商户查询这就是 N1。要解决需要在 Specification 里加root.fetch(merchant, JoinType.LEFT)或者实体上用EntityGraph。这一点是新人在 JPA 上最容易摔的地方。5.3 用 MyBatis XML 实现同样的查询MyBatis 版本就直白很多SQL 可见条件自己拼分页自己处理这里用 LIMIT 演示也可以接 PageHelper。Mapper 接口public interface OrderMapper { ListOrderVO pageQuery(Param(query) OrderQuery query, Param(offset) int offset, Param(pageSize) int pageSize); Long countQuery(Param(query) OrderQuery query); }XML 映射文件注意where标签自动处理的 AND 前缀和if判空逻辑select idpageQuery resultTypecom.example.vo.OrderVO SELECT o.order_no, o.amount, o.status, m.name AS merchant_name, o.created_at FROM t_order o LEFT JOIN t_merchant m ON o.merchant_id m.id where if testquery.merchantId ! null AND o.merchant_id #{query.merchantId} /if if testquery.status ! null AND o.status #{query.status} /if if testquery.startTime ! null AND o.created_at gt; #{query.startTime} /if if testquery.endTime ! null AND o.created_at lt; #{query.endTime} /if /where ORDER BY o.created_at DESC LIMIT #{offset}, #{pageSize} /select select idcountQuery resultTypelong SELECT COUNT(*) FROM t_order o where if testquery.merchantId ! null AND o.merchant_id #{query.merchantId} /if if testquery.status ! null AND o.status #{query.status} /if if testquery.startTime ! null AND o.created_at gt; #{query.startTime} /if if testquery.endTime ! null AND o.created_at lt; #{query.endTime} /if /where /select这段代码清晰展示了 MyBatis 的优点和缺点优点是你一眼能看出 SQL 是什么样LEFT JOIN 的语义、WHERE 条件的取舍完全自己掌控查询优化空间巨大缺点是你要自己写 count 查询自己处理分页的 offset 计算。5.4 混合写法在 Service 层统一编排混合方式并不神秘——本质是根据操作特性选择工具。我用 JPA 的 Repository 负责订单创建和主键查询用 MyBatis 的 Mapper 负责列表和统计查询。Service 方法里通过同一个事务管理器协调代码结构依然清晰Service public class OrderServiceImpl implements OrderService { // JPA 部分负责写操作 private final OrderRepository orderRepository; // MyBatis 部分负责复杂查询 private final OrderMapper orderMapper; Transactional public Order createOrder(OrderCreateCommand cmd) { Order order new Order(); // ... 字段赋值 // 明细级联保存 order.setItems(cmd.getItems()); return orderRepository.save(order); } Transactional(readOnly true) public OrderDetail getOrderDetail(Long orderId) { return orderRepository.findById(orderId) .orElseThrow(() - new BizException(订单不存在)); } Transactional(readOnly true) public PageResultOrderVO pageQuery(OrderQuery query) { long total orderMapper.countQuery(query); int offset (query.getPage() - 1) * query.getPageSize(); ListOrderVO list orderMapper.pageQuery(query, offset, query.getPageSize()); return PageResult.of(list, total); } }这种写法的核心价值在于你不需要强迫一个框架去干它不擅长的事。JPA 负责它能优雅建模的对象写操作MyBatis 负责它天生擅长的动态 SQL 查询Service 层作为编排者把两者协调起来。只要保证一个事务方法里不要交叉使用两套框架操作不同数据源这套方案跑得非常稳。6. 常见问题与排查技巧实录6.1 混合架构下事务失效的 3 个典型场景事务失效是混合架构里最常见的疑难杂症。我把实际排过的坑列举出来按遇到频率排序同一个类内部调用Service 方法 A无事务调用了本类的另一个方法 B有TransactionalSpring AOP 代理生效的前提是走代理对象类内部调用走的是 this 引用事务注解完全不生效。判断办法是在日志里观察是否有 Connection 的开启和提交记录或者直接断点看 SqlSession 的自动提交标志。多数据源但只配了一个事务管理器当项目里配了多个 DataSource比如读写分离或分库但Transactional没有指定具体的事务管理器Spring 会默认选择一个另一个数据源的写操作根本不加入当前事务。解决办法是在注解上显式指定transactionManager或者用Transactional(transactionManager xxxTxManager)。MyBatis 和 JPA 操作的是不同的 DataSource即使你用了全局事务管理器如果 MyBatis 的 SqlSessionFactory 指向 DataSource AJPA 的 EntityManagerFactory 指向 DataSource B那么单个Transactional是管不住两个数据源的。跨数据源强一致必须引入分布式事务组件这一点没有捷径。6.2 JPA N1 查询的识别与修复JPA 的 N1 查询是性能杀手但识别起来并不难。最笨也是最有效的办法是打开 SQL 日志观察一次列表查询之后是不是跟着大量相同结构的单条 SELECT。在 Spring Boot 里配置spring.jpa.show-sqltrue spring.jpa.properties.hibernate.format_sqltrue logging.level.org.hibernate.SQLdebug logging.level.org.hibernate.type.descriptor.sqltrace日志一旦打出 100 条相同结构的select * from t_merchant where id?N1 实锤了。修复方案有三个层级最直接ManyToOne(fetch FetchType.EAGER)或查询里JOIN FETCH。更优雅EntityGraph在 Repository 方法上声明实体图指定要预加载的关联路径。建模层把跨实体的查询放到单独的数据访问方法中用“组装 DTO”而非“懒加载导航”的方式避免触发 N1。我的经验是列表页一律用 MyBatis 或 Query 原生日击杀查询结果直接映射为 VO详情页可以用 JPA 的懒加载但前提是页面只在极少数需要时才获取关联数据。6.3 MyBatis 一级缓存导致的数据不一致MyBatis 的一级缓存默认开启范围是 SqlSession。在一个 SqlSession 里执行两次相同的查询第二次会直接命中缓存不查数据库。问题在于如果两次查询之间有另一个事务更新了这条数据你不会看到最新的值但你的业务代码还以为是“实时查询”。我曾经在开发一个库存扣减模块时踩过这个坑先查库存数量然后执行扣减再查库存数量结果第二次查出来还是扣减前的值因为两次查询在同一个 SqlSession 里。排查过程费了不少劲最后通过全局配置localCacheScopeSTATEMENT解决也可以手动调用sqlSession.clearCache()。但更根本的解决方案是写操作和读操作不要放在同一个事务的同一个 SqlSession 里复用特别是库存、金额这类强一致性的数据。这里给一个常规排错建议MyBatis 查询偶尔结果不更新先查是不是缓存问题。你可以临时配置mybatis.configuration.local-cache-scopestatement验证如果更新后正常了再找具体的 SqlSession 复用位置。6.4 我的避坑经验速查表下面这张表是我在实际项目中沉淀下来的按组件分类列出问题、现象和解决思路建议收藏后对照自查问题场景现象排查思路解决方式JPA 关联查询超级慢列表接口耗时 2 秒打开 SQL 日志看查询次数JOIN FETCH / EntityGraph / 改用 MyBatis 查询混合架构下数据不同步一个操作成功、另一个失败检查两个框架是否同一数据源、事务管理器是否同一个统一 PlatformTransactionManager / 引入分布式事务MyBatis 结果不实时同一方法内两次查询结果不一致检查是否命中一级缓存localCacheScopeSTATEMENT / clearCacheJPA 实体修改自动更新意外 UPDATE 语句检查实体是否处于托管状态属性被意外修改在 Service 层及时处理快照或禁用 OpenSessionInViewMyBatis XML 动态 SQL 报错多余 AND 或括号不匹配渲染后 SQL 打出来看MyBatis 打印 SQL 后复制到客户端单独执行调试分页结果错乱总数对、列表不对检查 COUNT SQL 和查询 SQL 的条件是否一致XML 里 count 与查询分开写但保持条件一致注意多表 JOIN 去重以 MyBatis 打印 SQL 为例这是一个非常实用的调试手段。在 Spring Boot 配置文件里加一行logging.level.com.example.mapperdebug然后 MyBatis 会把 Mapper 接口的完整 SQL 渲染结果和参数打印在日志里你把 SQL 原样复制出来到数据库客户端手动执行就能确定问题在 SQL 本身还是参数传递环节。这个方法不仅适用于 XML 查询同样适用于注解 Select 的场景。7. 最后说点掏心窝的话说实话技术圈对 ORM 的争论从来不缺热度但你翻翻真实的招聘要求和公司现状会发现MyBatis 和 JPA 在国内市场其实各有半壁江山。一些老牌互联网公司的核心交易系统基本都是 MyBatis 系因为这是当年 iBatis 时代的技术惯性加上 DBA 管控 SQL 的需求而外资背景或新组建的团队很多会选 Spring Data JPA因为开发迭代速度太快了CRUD 用 JPA 确实省力。你问我到底该选谁我的答案始终是先看团队再看项目最后才看技术本身。如果你正处于选型的十字路口我给你一个可以“抄作业”的判断流程如果你的团队全员能熟练写复杂 SQL且项目涉及大量报表、统计、多表关联查询无脑选 MyBatis有条件的直接上 MyBatis-Plus 简化 CRUD如果你的团队对领域模型有深刻理解且项目以简单的增删改查和内部管理功能为主JPA 会给你带来极高的开发效率如果你做的是大项目既有复杂交易链路又有管理和查询页面那不用纠结混合架构就是答案按我之前讲的原则把边界划清楚把事务和缓存这两个雷区排干净这套方案能在生产环境稳定跑很多年。最后送一个小技巧不管最终选哪种方案请务必把打印 SQL 的开关提前配好这会在开发、测试、排查问题的全程帮你省下无数时间。等你被线上一个诡异的数据问题折磨得头疼时你会发现能一眼看到 SQL 是多大的幸福。
返回列表