news 2026/9/8 11:31:51

三大ORM框架性能实测:MyBatis-Plus、JPA与Spring Data JDBC对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
三大ORM框架性能实测:MyBatis-Plus、JPA与Spring Data JDBC对比

1. 为什么还要做一次ORM性能测试:动机与目标

先说结论:我这次跑Benchmark,不是想证明某一个ORM天下第一,也不是想搞一个"碾压全场"的流量标题。原因其实很朴素——团队内部对选型有分歧,吵了两个月没结果,上一版测试报告又被人指出用例设计有问题,比如没有区分冷热查询、没有关闭JPA的一级缓存、批量写入用的是逐条insert而不是batch。所以这次"最终版"本质上是一次翻案重测,把之前所有被质疑的点全部修正,用一套能经得起推敲的方法重新跑数据。

做任何性能测试之前,先想清楚一个问题:测出来之后,这个数字要支持什么决策?我们团队当时有三个候选对象:MyBatis-Plus、Spring Data JPA(Hibernate实现)、Spring Data JDBC。数据库是Oracle 19c,Java版本是17,Spring Boot是3.2.x。有人追求开发效率,觉得JPA最合适;有人在意SQL可控性,坚持MyBatis;也有人提出Spring Data JDBC作为中间路线,轻量、不用写XML、又没有JPA那么重的实体生命周期。

本文面向的读者,是那些正在做技术选型、或者想验证自己当前用的ORM到底有没有性能瓶颈的开发者。我不会只丢一张平均值表格,而是把测试设计思路、每个场景为什么这么设计、测试过程中踩到的坑、数据背后的真实含义都摊开讲。因为性能测试最忌讳的就是"拿着一个不好用的方案测出一个漂亮数字",这种报告不仅没有参考价值,还会误导整个团队。

开始之前先把本次测试的技术边界定清楚:只测关系型数据库Oracle 19c的场景,不涉及MongoDB、Elasticsearch等其他存储的对比,也不讨论生产环境里的网络延迟和中间件开销——那些属于全链路压测的范畴,Benchmark阶段关注的是"同一台机器上,ORM本身带来的CPU和响应时间差异"。

2. 测试方案的修正过程:上一版被质疑的四个设计缺陷

既然叫"最终版",就说明之前有"非最终版"。上一轮测试报告被架构组打回来,主要指出了四个问题,我逐个说明,也方便你用同样的思路审视自己的Benchmark。

第一个问题:没有区分冷查询和热查询。上一版测试里,每条SQL都是第一次执行,所有结果都是"冷"的。但数据库有buffer cache,应用层有各种缓存,生产环境中大部分查询命中的是热数据。"冷热不分"最直接的后果是:底层走全表扫描的SQL和走上万次执行后数据全在内存的SQL,测出来的响应时间能差几十倍,而这种差异跟ORM本身没有关系。这次我分开测了冷缓存查询热缓存查询两种模式,冷模式清空共享池和buffer cache,热模式先预热执行500次再计时。

第二个问题:JPA的一级缓存没有关闭。这是一个非常隐蔽的坑。Hibernate的Session一级缓存在同一个事务内是默认开启的,如果你在测试方法上加了@Transactional,循环里查同一条记录100次,实际上只有第一次真正发SQL,剩下99次全从一级缓存里取。上一版测试里的"JPA单条查询性能"就是这么被抬高的——不,严格说不是抬高,是测的根本不是查询性能,而是缓存命中性能。这次强制使用Session.clear()清理一级缓存,确保每次查询都真实穿透到数据库。

第三个问题:写入测试用了逐条insert。上一版三个框架都是循环里逐条insert,这在MyBatis和JPA下都是最差的写入方式,而且测试结果只反映了"框架封装SQL的固定开销",根本没有测到批量提交的能力。这次增加了一个批量写入场景,MyBatis用ExecutorType.BATCH,JPA用saveAll走JDBC batch,Spring Data JDBC用JdbcAggregateTemplate.batchInsert,三种方案各自用正确的方式去测。

第四个问题:没有控制连接池参数的差异。上一版HikariCP的最大连接数、最小空闲数没有统一。测试并发是32线程,JPA那边给了50个连接,MyBatis那边只给了20个,这直接导致MyBatis在高并发下排队等待连接,数据自然不好看。这次所有框架统一使用maximum-pool-size: 32minimum-idle: 32connection-timeout: 5000ms,避免连接池本身成为变量。

这四个问题修正之后,我有信心这次的数据至少是"公平的"。性能测试最怕的不是跑出来的数据不好看,而是数据好看但测的根本不是所有人都以为的那个东西。

3. 测试框架、环境与数据准备:把变量压到最少

3.1 为什么选JMH而不是自己写循环计时

很多人做性能测试喜欢自己写个for循环,System.currentTimeMillis()包一下,运行完一除就出结果。这种做法的最大问题是JVM的JIT编译优化。一段代码跑了上千次之后,热点代码会被编译器编译成机器码,性能可能提升一个数量级。如果预热时间不够,测出来的是"解释执行阶段的性能",生产环境根本不会那么跑。

JMH(Java Microbenchmark Harness)是专门解决这类问题的工具,由JVM性能调优专家Aleksey Shipilëv主导开发。它提供了@Warmup@Measurement注解来控制预热和正式测量的迭代次数,还能通过@Fork指定fork几个独立JVM进程来消除跨迭代的干扰。这次我设置了@Warmup(iterations = 5, time = 3)@Measurement(iterations = 5, time = 3),每个benchmark方法都fork一个独立的JVM进程。跑完一轮大概需要四十多分钟,为了保证稳定性,测试机在跑分期间不允许其他人登录。

3.2 硬件与版本信息

项目配置
CPUIntel Xeon Gold 6248R(4核分配)
内存16GB(JVM堆-Xms6g -Xmx6g -XX:+UseG1GC)
数据库Oracle 19c(19.16),单实例
应用框架Spring Boot 3.2.5
JDKOpenJDK 17.0.10
连接池HikariCP 5.1.0
网络本机回环地址(排除网络开销)

所有测试都跑在应用进程与数据库进程在同一台物理机的环境里,走localhost连接。这样能让网络栈的干扰降到最低,测出来的差异更接近"ORM本身的计算和SQL执行差异"。不过也要提醒一点:真实生产环境走网络传输时,网络延迟通常会掩盖掉一部分微秒级别的框架差异,这点我在后面数据解读部分会再提。

3.3 表结构与数据规模

为了贴近真实业务而不是"教科书式"的单表CRUD,我建了一张t_order_detail订单明细表,有12个字段,包含idorder_nouser_idsku_idqtypricestatuscreate_timeupdate_time等。索引方面:主键索引、user_id普通索引、order_no唯一索引。数据量定为50万行,分布在1000个用户下,每个用户500条订单明细。这个规模不大,但已经能明显看出不同ORM在批量写入和列表查询时的CPU开销差异了。

测试期间Oracle的SGA设置为2GB,PGA设置为1GB。每次冷查询测试前,通过ALTER SYSTEM FLUSH BUFFER_CACHE; ALTER SYSTEM FLUSH SHARED_POOL;清理缓存。热查询则先连续执行500次相同SQL完成预热,再开始计时。

4. 六大测试场景与实测数据:最终版的结果出来了

每个场景都是五个迭代取中位数,因为中位数比平均值的抗干扰能力强。测试的结果以JMH自带的Score作为响应时间,单位是us/op(微秒每次操作),数值越小代表单次操作耗时越低。同时记录了GC相关指标,确保没有因为内存分配问题导致偏差。

4.1 场景一:主键单条查询(热缓存)

这条SQL非常简单,WHERE id = ?查一条订单明细。热缓存模式下,Oracle的buffer cache里已经缓存了该数据页,执行计划的物理读接近零。

框架响应时间(us/op)相对JDBC的倍数
JDBC(基线)52.41.0x
Spring Data JDBC68.11.3x
MyBatis-Plus71.91.4x
Spring Data JPA(Hibernate)96.31.8x

单纯看主键查询,差距并不夸张。JDBC最快,Spring Data JDBC紧随其后,MyBatis-Plus略高一点,JPA比JDBC慢了将近一倍。但注意,这只是微秒级别的差异,对单次用户请求而言,这一两微秒的差距完全被业务逻辑和序列化消耗淹没了。这一场景真正的价值在于看"框架自身的管理开销",Hibernate的实体生命周期管理和脏检查机制确实会带来额外CPU消耗,这就是它排在末尾的原因。

4.2 场景二:主键单条查询(冷缓存)

清空buffer cache后,查询必须走磁盘读取和语法解析。这个场景下的数据和4.1场景完全不同量级:

  • JDBC:1,085.6 us/op
  • Spring Data JDBC:1,090.2 us/op
  • MyBatis-Plus:1,120.8 us/op
  • Spring Data JPA:1,267.1 us/op

冷缓存下,数据库物理IO和解析SQL的耗时占比极高,ORM的差异反而被压缩了。JPA依然最慢,但慢的原因和之前不一样——Hibernate额外的SQL预解析、参数绑定步骤在冷环境下被放大了。这个数据也说明了为什么我说只测冷结果会掩盖框架差异,只测热结果会夸大框架差异,两种模式必须同时看

4.3 场景三:按user_id分页列表查询

WHERE user_id = ? ORDER BY id OFFSET ? ROWS FETCH NEXT 20 ROWS ONLY,这个场景比较贴近后台管理系统的列表页。为了公平,所有框架都让数据库感知分页,而不是查全量再在应用层分页。MyBatis-Plus用Page对象,JPA用PageRequest,Spring Data JDBC用Pageable

框架第一页响应(us/op)翻页到100页响应(us/op)
JDBC526.2642.8
Spring Data JDBC594.5707.6
MyBatis-Plus623.3748.2
Spring Data JPA887.61,082.4

分页测试里JPA的短板开始明显了,翻到第100页时已经超过1毫秒。这里有个额外发现:Hibernate生成的分页SQL在某些Oracle版本下会使用三层嵌套子查询,而MyBatis手写的分页SQL可以精确控制执行计划。JPA的封装能力确实是双刃剑——日常简单分页够用,但一旦表结构复杂、关联多,生成的SQL可能就是性能瓶颈。

4.4 场景四:按user_id批量查询(IN查询)

WHERE user_id IN (?, ?, ... 50个),一次查出50个用户的订单数据,每个用户500条,总数据量接近25万行。这个场景压力很大,测的是ORM的结果集映射能力和对象创建开销。

  • JDBC手动映射:15,423.8 us/op
  • Spring Data JDBC:16,851.2 us/op
  • MyBatis-Plus(默认映射):17,082.5 us/op
  • Spring Data JPA:23,456.9 us/op

这个差距就值得重视了。25万行的结果集映射,JPA比JDBC多出8毫秒,比MyBatis-Plus多出6毫秒。原因在于Hibernate需要为每一行创建实体对象并纳入持久化上下文管理,即使你没有修改任何字段,它也要检查脏数据。如果项目里存在大量这种"查询大结果集然后处理"的场景,JPA的单线程吞吐确实是硬伤。

4.5 场景五:单行插入

在自增主键(Oracle的IDENTITY列)下,三种ORM插入单条记录的耗时:

框架响应时间(us/op)
JDBC834.2
Spring Data JDBC892.7
MyBatis-Plus918.5
Spring Data JPA1,052.3

单行插入时,主键的获取方式影响很大。JPA因为有实体生命周期管理,插入前后要维护持久化上下文状态,所以开销最高。另外测试中发现,JPA的ID生成策略如果配的是GenerationType.SEQUENCE,在Oracle下每次insert前都会执行一次序列的nextVal查询,相当于多一次往返;如果改成GenerationType.IDENTITY则无此问题。这个细节很多博客都没提到,团队里有人踩过坑,我特意在这里讲清楚。

4.6 场景六:批量写入5000条

这是所有场景里最残酷的。使用各自框架推荐的batch方式,每批500条,共10批。数据如下:

框架批量写入耗时(ms)相对JDBC的倍数
JDBC(executeBatch)486.71.0x
Spring Data JDBC532.41.1x
MyBatis-Plus(BATCH执行器)618.91.3x
Spring Data JPA(saveAll)1,204.52.5x

JPA在批量写场景下几乎没有任何优势。saveAll看起来像批量操作,但如果实体的ID生成策略是SEQUENCE,Hibernate要先逐个获取ID再执行insert,整个过程退化成"模拟批量"——实际还是一行一行插入。这是Hibernate的一个老问题,即使Hibernate 6做了不少优化,批量场景依然没法跟真正的JDBC batch比拼。如果你项目中恰好有"定时任务批量同步数据"这种需求,选JPA大概率要吃亏。

另外要补充内存分配差异:本次测试中我从GC日志观察到,JPA在批量写入时Young GC次数明显高于MyBatis-Plus,每分钟多出约12次GC。原因是Hibernate的快照机制需要为每个实体保存一份原始状态副本用于脏检查,这批额外的对象分配就是GC压力的主要来源。GC频繁意味着CPU片被浪费在垃圾回收上,这在低延迟场景下是不可接受的。

5. 数据背后的逻辑:为什么会有这几倍性能差异

5.1 JPA/Hibernate的"下凡"成本

很多人对JPA有一个误解:以为用了JPA就等于用了Hibernate,而Hibernate是"重量级ORM"所以一定慢。实际上,慢的主要来源不是ORM这个词,而是持久化上下文(Persistence Context,即一级缓存)的维持成本。当你调用find方法时,Hibernate不只是执行了一条SQL,它还要:

  1. 检查持久化上下文中是否存在该对象
  2. 如果不存在,发起SQL查询
  3. 将查询结果映射为实体对象
  4. 把实体放入持久化上下文,并为其生成一个快照
  5. 如果实体有级联关联,还要处理关联对象的加载

这五步中只第一步和SQL直接相关,其余全是框架额外开销。小数据量下这个开销不痛不痒,但当你查询上万行结果集并做计算时,每行都要过一遍这套流程,累计开销就被放大了。这就是我在4.4场景看到的8毫秒差距的来源。

5.2 MyBatis/MyBatis-Plus的优势与隐藏成本

MyBatis-Plus的定位很聪明:它不管理实体生命周期,只完成"SQL结果到对象的映射"。没有一级缓存快照,没有脏检查,省掉了一大块CPU开销。这也是为什么MyBatis-Plus在批量读取场景下最接近JDBC——它本身就是"增强型JDBC"。

但这个轻量是有代价的。MyBatis-Plus的动态SQL需要通过XML或注解字符串来定义,编译器无法帮你检查SQL正确性,一旦字段名变了,运行期才报错。另外它的二级缓存默认也是关闭的,如果你不注意,相同查询在短时间内反复执行,数据库压力会很大。好在生产环境通常都有Redis等外部缓存层,ORM这层的缓存本来就不是很关键。

5.3 Spring Data JDBC:被低估的中间选项

Spring Data JDBC是Spring生态里一个"轻量级聚合根持久化"方案,它的设计目标就是砍掉JPA的复杂生命周期,只保留基础的CRUD。测试结果也印证了它的定位——在大部分场景下它只比MyBatis-Plus慢一点点,但不需要写XML映射文件,实体类加注解就能用。如果你的项目里关系模型不复杂、也不想引入MyBatis那套XML体系,Spring Data JDBC值得认真考虑。

当然它有明显的使用边界:对复杂动态查询支持弱,多对多关联需要自己管理中间表,分页能力也远不如MyBatis-Plus的Page插件灵活。简单说,它是"年轻团队的舒适区",但不适合复杂遗留系统迁移。

6. Benchmark过程中踩过的坑与排查链路:帮你省两小时的迷惑时间

性能测试的项目里最怕的不是框架慢,而是你根本不知道自己在测什么。下面这几个坑实打实浪费了我不少时间,按排查链路写出来,希望你能绕过。

6.1 坑一:JPA测试方法加了@Transactional,一级缓存悄悄"作弊"

现象:单独测JPA单条查询,90微秒每次,看起来还行,好像也没有比JDBC差很多。

排查链路:先看数据库的SQL日志,发现executeQuery的调用次数远远少于测试次数。于是怀疑SQL没有真正发到数据库。把spring.jpa.show-sql=true打开,在测试方法里循环查100次相同主键,日志里只出现了1条select。这时才意识到问题出在@Transactional——事务不提交,一级缓存不清除,后面99次查询命中缓存,自然快得离谱。

解决办法:去掉测试方法上的@Transactional,并且每次查询后调用EntityManager.clear()。对于MyBatis,默认SqlSession一级缓存是开启的,测试时最好也为每个循环创建新的SqlSession,避免同样的假象。

6.2 坑二:JPA ID生成策略配成了SEQUENCE但没配allocationSize

现象:批量插入5000条,JPA用了2.3秒,而JDBC只要0.5秒。差距太大了,不像是正常的框架开销。

排查链路:打开SQL日志,发现每条insert前都跟着一条select seq.nextval from dual。这就是"每插入一行都要去数据库取一次ID",五千次插入等于有一万次数据库往返。这个问题的根因是@GeneratedValue(strategy = GenerationType.SEQUENCE)配置下,默认的allocationSize是50,如果你把数据库里sequence的步长设为1(Oracle默认),Hibernate拿到的ID会超出实际可用范围,就可能导致主键冲突。

解决办法:要么把allocationSize配置为与数据库序列步长一致,要么直接改用GenerationType.IDENTITY。如果业务上允许,也可以在Hibernate里配置CALL方式让数据库在insert语句中用RETURNING子句返回ID,减少一次往返。

6.3 坑三:HikariCP连接池参数不一致导致"被排队"拖慢

现象:第一轮测试里MyBatis-Plus在32并发下性能远低于预期,甚至比单线程还慢。CPU占用率很低,数据库连接使用率很高。

排查链路:用SHOW PROCESSLIST;看数据库会话,发现大量Sleep状态连接。再查应用端的连接池状态,Unused连接数一直是0,Active连接数一直32。原来MyBatis测试的配置里maximum-pool-size没改,还是上一轮的20,而并发是32,剩下12个线程在connection-timeout期间排队等待。这个排队等待时间并没有体现在SQL的执行时间上,而是在getConnection调用里消耗掉了。

解决办法:统一所有测试的maximum-pool-size与并发数一致。JMH测试时线程数从@Threads(32)传入,连接池参数必须大于或等于线程数,最好等于线程数,避免线程被连接池限流。

6.4 坑四:Oracle的共享池共享SQL导致统计结果漂移

现象:同一场景跑了两轮,第二轮比第一轮快了20%。一开始以为是JIT或数据库buffer cache的影响,后来排查发现是Oracle共享池里的游标共享在做怪。

Oracle对完全相同的SQL会做游标软解析复用,所以第二轮测试时,很多SQL的解析开销已经不需要了。要避免这个问题,一种方法是在SQL里注入一个每次都不一样的注释,让Oracle认为这是新SQL,强制走硬解析;另一种是每次测试都清空共享池。为了保证公平,我在所有"冷缓存"测试场景都执行了ALTER SYSTEM FLUSH SHARED_POOL,确保每轮都是从零解析,这样就不存在"第二波打表"的问题。

6.5 坑五:JIT编译对短时测试的干扰

现象:第一轮测试时Spring Data JDBC表现比MyBatis-Plus慢很多,但第二轮数据接近持平。

排查链路:JMH日志里看到# Fork: 1/2等字样,第一轮fork的JVM进程刚启动跑测试时还没进入稳定的JIT优化状态。JMH的@Warmup迭代跑完后才能进入正式测量,这个策略是对的,但如果你的测试是自己写循环而不是用JMH,预热不足的情况会非常普遍。短测试(小于5分钟)尤其不可靠,因为JIT编译不稳定。

解决办法:严格按JMH的方式设计,确保在正式测量之前有足够的预热量。或者,干脆用一个长期运行的服务应用(打成Spring Boot jar跑起来)做外部计时压测,这样更接近生产形态,不过要注意引入的干扰因素会更多。

7. 性能之外的真实取舍:几个值得思考的选型问题

7.1 性能是不是唯一的决策标准?

从数据上看,JDBC几乎处处最快,但显然不可能为了性能把项目里所有持久化层都换成手写JDBC。开发效率、代码可维护性、团队熟练度、动态查询能力,这些维度和性能一样重要。在做技术选型时,正确的做法不是"谁快选谁",而是画出你自己的权重雷达图,把性能作为其中一个维度,和其他因素一起评分。

以我们团队为例,最终的选择是:主业务模块用MyBatis-Plus,因为它SQL可控、批量能力强,团队里大部分人有MyBatis经验,学习成本低;报表查询模块用jOOQ(这次测试没有包含,因为当时还没引入),它在类型安全的动态SQL方面体验很好,适合拼接复杂查询;后台告警管理这种简单应用用Spring Data JDBC,十几个字段的小表CRUD,用不上MyBatis那套XML体系,Spring Data JDBC更轻、更直观。

7.2 99分位延迟比平均值更重要

上面的表格里我列的都是中位数(50分位)。但生产环境下,真正影响用户体验的是P99甚至P999的延迟。在这点上,GC停顿和冷查询带来的长尾效应远比ORM本身的平均数差异更值得关注。

我简单统计了JPA在批量写入场景下的GC日志,一个有意思的发现是:每次Young GC停顿约3-5ms,批量写入5000条数据的完整流程中累计停顿约8-10ms。也就是说,JPA比JDBC多出的那700毫秒里,有一小部分其实是GC停顿贡献的,而不是单纯的框架计算开销。如果你的业务里存在大结果集查询和批量写入,选型时一定要兼顾GC表现,不然上线后你会发现"框架看起来没慢多少,但GC报表很难看"。

7.3 "ORM慢"也许不是ORM的问题

测试过程中有几组数据我反复验证过,发现某一条查询慢得离谱,但换ORM也一样慢。查了执行计划后才定位到问题出在索引缺失或者隐式类型转换上——例如Oracle里VARCHAR2字段和NUMBER比较时,如果没有正确的转换,可能触发全表扫描或索引失效。这提醒我们,在怀疑ORM框架性能之前,先确认SQL和索引层面没有硬伤,否则再换框架也无济于事。

一个印象很深的例子:WHERE user_id = '10001',如果user_idT_NUMBER而SQL里传的是字符串,而列上又没建函数索引,Oracle会把列隐式转成字符串比较,从而导致索引失效。这种情况下你把框架从JPA换成MyBatis,性能一点也不会变好,因为问题根本不在ORM。性能排查的第一原则:先看执行计划,再谈框架选型。

8. 把性能测试融入日常工作的方法

8.1 给项目添加一套"性能回归基准"

选型完成后,性能测试不应是一次性的。我见过太多团队在选型时跑得勤快,上线后就把Benchmark丢到脑后。更科学的做法是:把核心场景的基准测试纳入CI流水线,每次代码合并前自动跑一遍关键Benchmark,设置一个性能阈值(比如P95响应时间不得超过基线值的120%),超了就阻止合并并提醒开发人员确认。

JMH完全可以作为自动化测试任务跑在CI的Job里,比如用Maven插件jmh-java或者简单的shell脚本触发。当然不是每个PR都跑全量,那样时间太长,可以只跑"主键查询+批量写入"这两个最有代表性的场景。

8.2 基础设施变化后要重测

数据库从Oracle迁移到PostgreSQL、升级Spring Boot大版本、或者JVM从11换成17,这些变化都会影响ORM的实际性能。特别是数据库切换——不同数据库对分页语法、批量插入、序列生成的支持不同,原来在Oracle上性能表现优异的方案在PostgreSQL上就未必了。别以为"ORM抽象了数据库差异"就万事大吉,抽象层只保证了"能跑",但没保证"跑得快"。

8.3 建立业务侧的性能基线

作为补充,我会为线上核心接口建立性能基线:记录每个接口在固定流量模型下的P50、P95、P99延迟和数据库QPS,每次发布后自动对比。如果发现接口变慢,再回看这次Benchmark的数据,能快速判断是不是ORM层引入了问题。两个视角互补:Benchmark看的是"框架上限",线上监控看的是"实际体验"。

9. 结论与个人建议:最终版测试之后我的选择

综合六大场景的测试结果,我来给出这次"最终版"测试的个人结论。先声明,这不是放之四海皆准的银弹建议,只是针对我们团队的项目特征——Spring Boot 3.2 + Oracle 19c,以常规CRUD和中小批量同步为主——做出的实际选择。

日常CRUD模块:我选MyBatis-Plus。理由是在热缓存主键查询、分页列表、批量读取这些最常见的场景下,它的性能和JDBC的差距都在可接受范围内,同时它还提供了QueryWrapper这种动态条件构造器,开发效率远比手写XML高。更重要的是,团队里大部分人原本就有MyBatis经验,背后的隐含成本最低。

复杂报表/动态SQL模块:我更倾向jOOQ或直接JDBC模板。这类查询往往需要精细控制SQL的结构、绑定参数、处理动态排序等高阶功能。用MyBatis的XML拼接复杂动态SQL会让XML越来越像"第二门编程语言",维护成本不可控。jOOQ能在编译期校验SQL语法,配合强类型API,在复杂查询场景下比MyBatis更安全。

纯轻量应用(某张独立的简单配置表):Spring Data JDBC。没有复杂的级联、继承或多态需求,用Spring Data JDBC最省事。它不像Hibernate那样有一大堆实体生命周期概念要理解,又比纯JDBC简洁,比MyBatis少一道XML配置。

Spring Data JPA:不是不能用,是得用得克制。如果是全新的、以对象建模为核心的业务,比如领域驱动设计实践得比较彻底、表结构由领域对象推导出来的项目,JPA依然有它的优势。但你必须认识到它的性能衰减点——大结果集、批量写、复杂分页——并提前设计规避方案:比如批量写改用EntityManager原生批量,或直接走JDBC模板。否则一旦项目规模上去,性能问题会让你很难受。

最终我的建议可以浓缩成三句话:先看SQL和执行计划,再谈框架差异;性能测试要冷热分开、缓存全关、连接池统一,才能测出真数;选型不要只盯着平均值差异,要把开发效率、SQL可控性、团队维护成本和GC压力都纳入考量。这次"最终版"的Benchmark帮我们终止了一场旷日持久的争论,希望这份完整的过程记录,也能帮你少走一点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 11:30:08

嵌入式Linux屏与安卓屏选型指南:开机速度、稳定性与成本全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:29:24

STM32F103 AB双分区OTA升级:基于FreeModbus与标准库的可靠实现

STM32F103做OTA已经不算新鲜事,但要把OTA做成A/B双分区、带完整回滚机制、还能通过Modbus RTU串口稳定传输固件,这组合确实少见。我这次是把FreeModbus v1.6移植到标准库v3.5环境下,配合自定义Bootloader,实现了基于RS232串口的A/…

作者头像 李华
网站建设 2026/9/8 11:29:10

PLC恒压供水系统5泵设计:PID控制与变频器切换全解析

1. 泵组规模背后的容量逻辑:为什么偏偏是5泵高层楼宇恒压供水这个领域,我接触过不少项目,坦白讲3泵、4泵很常见,5泵属于偏复杂的系统了。这个"No.928 西门子S7-200 PLC高层楼宇恒压供水系统变频供水5泵五泵"里的5泵方案…

作者头像 李华
网站建设 2026/9/8 11:28:44

ComfyUI节点式AI绘画:从零搭建到高效工作流实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:28:13

DeepSeek Harness实战:将DeepSeek接入Codex CLI与Claude Code的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:27:46

OpenCode实战:开源终端AI编程助手的安装、配置与效率秘籍

这段时间 AI 编程助手的圈子是真的热闹,codex 刚火完,claude code 又来了,然后 opencode 这个名字开始隔三差五出现在我时间线上。我本来没太当回事,直到身边好几个做后端和全栈的朋友同时推荐,才抱着试试看的态度装了…

作者头像 李华