
目录一、选的不是三个框架是控制权放在哪一层一争吵的背后二为什么「简单用 JPA、复杂用 MyBatis」不够用三动手选型之前先量出来的六个数1、按主键存取占比2、复杂查询占比与形状3、模式变更频率4、写路径的批量规模5、SQL 审计与治理要求6、团队画像二、三层抽象各自把什么藏了起来从一条 SQL 到一个对象的翻译链一JDBC所有决定都在你手里包括你不想做的那些二MyBatisSQL 留在你手里映射与会话交了出去三JPA交出去的不是 SQL是「什么时候发 SQL」四控制权移交清单一张对照表三、SQL 控制权管得住的三个层面一写得出语句形状由谁决定二看得见评审与审计的可见性三改得动优化建议的落地成本四、开发效率省下的时间从哪里来又在哪里还回去一样板代码账本一张表从建表到可用要写多少行二模式演进时的修改面三效率曲线的翻转点五、复杂查询分野真正的位置一复杂查询的五种形态二JPA 的三条出路与各自代价三MyBatis 动态 SQL 的强项与失控点六、性能框架差距小于默认值差距一性能差异的四个真实来源二公平对照实验怎么做七、团队成本最贵的不是学习曲线是误用曲线一两条曲线二可评审性与知识分布八、选型判据把四个维度合成一条决策路径一六类典型场景的直接判定1、标准业务中台以 JPA 为主干2、运营与报表后台以 MyBatis 为主干3、账务与交易核心SQL 可见方案倾向 MyBatis 或模板化 JDBC4、高吞吐批处理与数据管道模板化 JDBC 或 MyBatis 批执行器5、小团队快速验证JPA 或「MyBatis 加增强层」选熟不选优6、多数据库产品化交付JPA 的少数硬优势区二混用策略与边界纪律三选错之后怎么换迁移成本清单九、把选型写进工程防线与规则一三道防线1、决策记录防线2、静态检查防线1.1、注入面规则1.2、边界规则1.3、规范默认值规则1.4、类型规则3、测试闸门防线二落地次序结语参考资料干货分享感谢您的阅读关于数据访问层选型流传最广的是「简单业务用 JPA复杂业务用 MyBatis追求极致性能用 JDBC」。这句话作为第一印象大体无害一旦被当成选型结论就会有问题说它简单的人没有算过 JPA 在模式演进时省下的修改面也没有算过它在深度分页与批量写入上还回去的调试时间说它复杂的人没有区分「复杂在查询形状」与「复杂在动态拼接」而这两种复杂对应的最优解并不相同说它性能好的人往往在用错误的默认配置压测另外两个框架而所有人都默认「团队成本」等于「学习成本」忽略了三个框架真正贵的地方都在误用之后的排障与返工上。我们不给「哪个更好」的排名而是把这次选型拆成一条可以逐步推进的判断链先看清三层抽象各自把什么控制权拿走了、又把什么责任留下了再沿 SQL 控制、开发效率、复杂查询、团队成本四个维度分别记账最后把四本账合成一条可执行的决策路径并给出混用边界与选错之后的迁移成本。选型的本质不是挑一个名字写进架构文档而是决定「SQL 的最终控制权放在哪一层、由谁行使、靠什么手段被看见」。一、选的不是三个框架是控制权放在哪一层一争吵的背后技术方案评审里出现「这个项目用 MyBatis 还是 JPA」这个议题时讨论通常在二十分钟内滑向两种结局要么各自列举用过的项目然后按嗓门表决要么搬出「简单用 JPA、复杂用 MyBatis」的通行说法草草收场。两种结局的共同问题是把四个性质完全不同的问题压缩成了一个名字之争。第一问是 SQL 控制权这个系统里执行的每一条语句它的形状连接方式、索引提示、锁范围、返回列由谁决定出了慢查询从监控里的语句反查到代码里的位置需要几步数据库管理员给出的改写建议落地要动几个文件、冒多大风险控制权说不清性能治理就没有抓手——你无法优化一条你不知道从哪里长出来的语句。第二问是开发效率一张表从建表到暴露完整的增删改查接口需要多少行人写的代码加一个字段要同步改几处这些重复劳动能不能被生成器或框架吃掉效率账算不清团队就会把时间花在写样板代码上然后用「忙」来证明人手不够。第三问是复杂查询报表、多维筛选、跨五张表的聚合、窗口函数、按条件动态增减的谓词——这些查询在候选方案里分别怎么写、写出来的东西能不能被评审、三个月后还能不能被读懂复杂查询处理不好最终的结局高度一致绕过框架直接拼字符串把选型时承诺的一切纪律全部作废。第四问是团队成本现有成员对候选方案的真实熟练度是「用过」还是「排过障」招聘市场上两种技能的供给如何新人误用框架造成的事故团队有没有能力在评审阶段拦住成本账不算选型就变成了给三年后的维护者挖坑。这四问的顺序有讲究控制权是地基它决定了后三问的讨论前提效率与复杂查询是两个方向的拉扯几乎所有选型纠结都发生在这两问之间团队成本是乘数它会把前三问的任何一个短板放大成长期负债。二为什么「简单用 JPA、复杂用 MyBatis」不够用这句通行说法的问题不在方向上——它的方向大体是对的——而在于它把两个关键判断留给了直觉而直觉在这两处恰恰最不可靠。第一处是「简单」与「复杂」的边界没有操作性定义。一个项目立项时几乎总是「简单」的十几张表、标准增删改查、单表为主。问题在于业务系统的查询复杂度不是常量而是随着运营需求单调递增的——上线三个月后一定会出现「按七个可选条件筛选并按三种口径汇总」的列表页。如果边界判断只在立项时做一次那么用 JPA 的项目会在第一个复杂报表出现时陷入两难用规范查询语言硬写得到一段没人愿意评审的构造器调用链退回原生 SQL则前期为实体关系建模投入的设计全部变成摆设。「简单」不是项目的属性是项目某个阶段、某个模块的属性——所以真正可执行的答案必须回答到模块级而这句话只回答到了项目级。第二处是它默认了「复杂查询」是 MyBatis 相对 JPA 的唯一分野掩盖了另外三条同样重要的分界线。其一是写路径的语义差异JPA 的托管实体、脏检查与刷写时机是一整套独立的心智模型与查询复杂度无关团队接不接得住这套模型是独立的判断其二是模式演进的修改面差异字段增删时三个方案的波及范围差别巨大而这与查询复杂度也无关其三是可观测性差异语句由框架生成还是由人书写直接决定了慢查询排障是「按文件名定位」还是「按生成规则反推」。只用一条分界线做决策等于在另外三条线上闭着眼睛。更实用的说法应当是按「谁需要看见并控制 SQL」来分层而不是按「难不难」来分档。需要被数据库管理员逐条审阅的语句、需要索引提示与锁控制的语句、形状会被运营需求频繁改动的语句放在 SQL 可见的方案里生命周期跟着聚合根走、以主键存取为主、形状稳定的读写放在对象映射方案里两者的边界画在模块上用工程手段依赖规则、目录约定固定下来而不是画在某次会议纪要里。三动手选型之前先量出来的六个数选型讨论容易变成立场表态因为参与者手里都没有数。下面六个数都能在一两天内从现有系统或原型里量出来量完之后讨论会自动从「你喜欢哪个」转向「这些数指向哪个」。1、按主键存取占比统计现有系统或按产品原型估算全部数据访问里「按主键或唯一键读写单条记录」的占比。这个数是对象映射方案的收益上限占比八成以上JPA 的身份管理、一级缓存与脏检查都在做有用功占比不到一半说明系统以集合查询为主对象映射的核心机制大部分时间在空转甚至在帮倒忙。2、复杂查询占比与形状数出多表连接超过三张、含聚合或窗口函数、含动态谓词的查询占比并区分两种形状形状固定的复杂报表、对账单语句长但稳定与形状动态的复杂多条件筛选谓词随入参增减。前者三个方案都能承接JPA 走原生查询即可后者是 MyBatis 动态标签与专门的查询构造器的主场也是纯 JPA 最痛的地方。3、模式变更频率统计过去半年数据库结构变更加字段、改类型、拆表的次数。变更频繁的系统里「加一个字段要改几处」这笔账权重很高JDBC 手写映射的修改面最大且编译器帮不上忙JPA 的修改面最小MyBatis 居中且结果映射与语句里的列名是两处需要同步的地方。4、写路径的批量规模估算最大的批量写入场景日终结算写多少行、导入任务一次多少条。批量规模在十万行以上的写路径JPA 的托管实体机制需要专门的合批配置与上下文清理纪律才能不劣化JDBC 批处理接口与 MyBatis 批执行器的心智负担更小。这个数决定了写密集模块要不要单独选一条技术路线。5、SQL 审计与治理要求确认组织层面的硬约束是否要求上线前所有 SQL 经数据库管理员评审是否有慢查询问责机制是否要求语句与代码位置可双向追溯审计要求严格的组织里「语句由框架运行期生成」本身就是合规摩擦这一条可以直接否决纯 JPA 方案或者要求为其配备语句快照测试作为补偿手段。6、团队画像区分「用过」与「排过障」团队里有几个人处理过 JPA 的懒加载异常、级联误删、脏检查性能问题有几个人维护过三千行的映射文件并做过动态 SQL 注入审查两个数都为零的团队无论选哪个都要把第一次踩坑的成本计入排期只有一边为零那么另一边就是现实约束——选团队排过障的那个通常比选纸面上更优的那个三年总成本更低。六个数量出来之后很多争论会自动消解按主键存取占比高、模式变更频繁、团队熟悉对象映射——指向 JPA 为主复杂动态查询多、审计严格、写批量大——指向 MyBatis 或模板化 JDBC 为主两组信号同时强烈——指向按模块混用而不是全局二选一。二、三层抽象各自把什么藏了起来从一条 SQL 到一个对象的翻译链三个方案经常被并排画成「三选一」但更准确的图景是一条纵向的抽象阶梯JDBC 是地基MyBatis 与 JPA 都建在它上面只是各自向上收走了不同的控制权、向下留下了不同的责任。看清每一层拿走了什么、藏起了什么四个维度的账才有地方记。一JDBC所有决定都在你手里包括你不想做的那些JDBC 是 Java 平台访问关系数据库的标准接口层它规定了驱动、连接、语句、结果集四个抽象其余一切留白。用原生 JDBC 写一次查询你需要亲手完成七个动作获取连接、创建预编译语句、逐个绑定参数、执行、遍历结果集、逐列取值并装配对象、在任何执行路径上确保资源关闭。一条五列的单表查询完整写下来在三十行上下其中真正表达业务意图的只有那条 SQL 和装配逻辑其余全部是仪式。public OptionalOrderSummary findByOrderNo(String orderNo) { String sql SELECT id, order_no, amount, status, created_time FROM t_order WHERE order_no ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, orderNo); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { return Optional.empty(); } OrderSummary o new OrderSummary(); o.setId(rs.getLong(id)); o.setOrderNo(rs.getString(order_no)); o.setAmount(rs.getBigDecimal(amount)); // 金额列必须走 BigDecimal o.setStatus(rs.getString(status)); o.setCreatedTime(rs.getTimestamp(created_time).toLocalDateTime()); return Optional.of(o); } } catch (SQLException e) { throw new DataAccessException(查询订单失败, orderNo 已脱敏, e); } }这段代码的价值在于它把三个框架都要回答的问题全部摆在了明处语句形状是什么第一行写死了、参数怎么绑定占位符防注入、类型怎么转换金额走BigDecimal、时间戳转LocalDateTime、资源什么时候释放两层自动关闭、异常怎么归类检查异常翻译成运行时异常。JDBC 的问题从来不是能力不足而是它把每一个决定都变成了每一个人的日常劳动——三十行乘以两百个查询方法就是六千行没有任何两行值得存在的重复代码而其中任何一处漏关资源或漏做类型转换都是一个潜伏缺陷。实践中的「用 JDBC」几乎从不指裸写上面这段而是指模板化封装框架提供的模板类把连接、语句、结果集的生命周期收走把异常统一翻译你提供 SQL 与一个行映射函数。模板化 JDBC 把三十行压到八行同时保留了 SQL 的全部控制权——它是本文后面讨论「JDBC 路线」时的实际形态。二MyBatisSQL 留在你手里映射与会话交了出去MyBatis 的定位是「SQL 映射框架」而不是「对象关系映射框架」这个措辞差异就是它的全部设计立场SQL 由人书写框架负责其余一切——参数从对象到占位符的绑定、结果集从列到对象属性的装配、语句的注册与路由、会话与事务的边界、缓存与插件扩展点。select idfindByCondition resultMaporderResultMap SELECT o.id, o.order_no, o.amount, o.status, o.created_time FROM t_order o where if teststatus ! nullAND o.status #{status}/if if testminAmount ! nullAND o.amount gt; #{minAmount}/if if testchannel ! null AND EXISTS (SELECT 1 FROM t_pay_record p WHERE p.order_id o.id AND p.channel #{channel}) /if /where ORDER BY o.created_time DESC /select这段映射文件体现了 MyBatis 的两个关键性质。第一语句形状对评审者完全可见连接方式、子查询、排序列都写在文件里数据库管理员可以直接评审这个文件慢查询日志里的语句可以按语句标识反查到文件与行号。第二动态部分被限制在标签语义内if、where、foreach这些标签生成的变化是可枚举的比字符串拼接安全得多——但注意#{}与${}的边界前者是预编译参数绑定后者是文本替换后者用在任何用户可控的输入上都是注入漏洞这是 MyBatis 团队纪律清单上永远的第一条。MyBatis 交出去的东西同样要看清会话生命周期、一级缓存的作用域、映射结果的装配规则、延迟加载的触发时机这些都由框架决定。其中一级缓存的默认行为同一会话内相同查询返回同一结果在与 Spring 集成、每次调用一个会话的常见配置下几乎不可见但在手动管理会话或本地事务较长的场景里会制造「读不到别人刚提交的数据」的困惑。另外要指出社区流行的增强层在 MyBatis 之上补齐了单表通用增删改查与条件构造器把 MyBatis 最枯燥的单表样板也吃掉了——这使得「MyBatis 开发效率必然低于 JPA」的说法在今天的工程现实里需要重新校准但增强层的条件构造器本质上是把 SQL 拼接搬进了 Java 代码复杂到一定程度后同样丧失可评审性它是效率工具不是复杂查询的答案。三JPA交出去的不是 SQL是「什么时候发 SQL」JPA 是 Java 生态的对象关系映射规范主流实现是 Hibernate工程中通常再叠加一层仓库抽象按方法名派生查询、分页排序、审计字段。很多比较文章把 JPA 概括为「不用写 SQL」这个概括错过了重点。JPA 真正收走的不是「写 SQL」这个动作而是三项更根本的控制权。第一项是语句的生成权。你声明实体与关系框架把对象导航翻译成语句。翻译规则是确定的但不是直观的一次关联属性访问可能是一次内存读取也可能触发一条补发查询一个集合的立即加载可能把结果行数变成两个集合大小的乘积。语句从「人写的文本」变成「规则的产物」排障方式随之从「读文件」变成「反推规则」。第二项是语句的发出时机。托管实体的属性修改不会立刻产生更新语句语句在刷写时刻由快照比较生成发出顺序由框架按动作类型重排与代码书写顺序无关。「保存」在 JPA 里是入队、刷写、提交三件事这套时机模型是 JPA 团队成本的真正来源——懒加载初始化异常、事务外访问代理、批量任务越跑越慢全部源自对时机模型的误解而不是对某个注解的记错。第三项是对象身份的管理权。同一持久化上下文内同一主键只对应一个实例上下文同时持有字段快照用于脏检查。收益是重复读取被折叠、更新语句自动生成代价是上下文本身有内存与比较成本批量场景必须显式分批清理否则性能随处理行数超线性劣化。public interface OrderRepository extends JpaRepositoryOrder, Long { OptionalOrder findByOrderNo(String orderNo); // 方法名派生查询 PageOrder findByStatus(String status, Pageable page); // 分页由框架翻译 Query(SELECT o FROM Order o JOIN FETCH o.items WHERE o.orderNo :orderNo) OptionalOrder findWithItems(Param(orderNo) String orderNo); // 显式抓取计划 }八行接口声明替代了 JDBC 路线里两百行的手写方法这是 JPA 效率优势最直观的呈现。但要同时看到第三个方法当默认翻译不满足需要时你必须用查询注解显式干预抓取计划——JPA 用得好的团队与用得差的团队区别不在会不会写第一二行而在知不知道什么时候必须写第三行。四控制权移交清单一张对照表把三层抽象在九个关键决定上的归属并排列出选型讨论就有了共同的事实基础。这张表上有两个容易被忽略的读法。第一从左到右不是「越来越好」也不是「越来越差」是责任换位置JDBC 把类型转换的责任放在每个开发者的每一次书写里JPA 把它放在实体映射的一次性声明里前者错一次是一个缺陷后者错一次是一类缺陷——集中声明降低了出错频率也放大了单次错误的波及面。第二越往右默认值越多而默认值是双刃的JPA 的关联默认抓取策略、序列主键的默认取值步长、刷写的默认时机每一个默认值都在无人过问时替团队做了决定。选 JPA 的团队实际上承诺了「我们会逐个审阅这些默认值」没有这个承诺选型清单上的效率优势会在生产环境里逐项兑换成事故。三、SQL 控制权管得住的三个层面四个维度里最常被喊出口号、最少被说清楚的就是「SQL 可控」。把它拆开控制权其实是三个独立的层面三个方案在每一层的表现并不同步。一写得出语句形状由谁决定第一层是书写权这条语句的连接顺序、索引选择、锁语义、返回列由谁决定。JDBC 与 MyBatis 在这一层是完全体——你写什么就执行什么包括数据库方言特性、优化器提示、FOR UPDATE的锁范围、窗口函数与公用表表达式没有任何翻译层截留。JPA 在这一层是分级的对象查询语言覆盖常见形状但不覆盖方言特性规范化的构造器查询与对象查询语言等价但书写成本更高原生查询拿回完整书写权代价是放弃了一部分对象装配的便利与跨数据库可移植性。这里有一个流传很广的误解需要纠正「JPA 写不了复杂 SQL」是错的正确的表述是「JPA 写复杂 SQL 时你实际上退回了 MyBatis 的工作方式却没有 MyBatis 的配套设施」——没有动态标签、没有集中的映射文件、结果装配到非实体对象需要额外声明。退路存在但退路上的工程体验决定了团队会不会用它而不是文档里有没有它。二看得见评审与审计的可见性第二层是可见权评审者与审计者能不能在代码合入之前看到将要执行的语句。MyBatis 的映射文件是这一层的标杆——语句集中、可搜索、可以按文件粒度指定评审人数据库管理员不需要懂 Java 也能完成评审。模板化 JDBC 的语句散在代码里可见性略降但仍是静态文本。JPA 的派生查询与对象导航产生的语句在源代码里不存在评审者看到的是方法名与实体注解看不到语句——这在多数团队里意味着 SQL 评审这道工序对 JPA 代码事实性失效。补偿手段是存在的打开语句日志在测试环境人工核对用语句断言测试把关键路径的语句条数与形状固化进用例用代理数据源在集成测试里捕获全部语句生成快照供评审。这些手段有效但要诚实计入成本——选 JPA 的团队若同时背着严格的 SQL 审计要求等于选择了「用测试设施重建可见性」这项持续投入这笔钱在选型会上很少被摆到桌面上。三改得动优化建议的落地成本第三层是修改权慢查询定位之后改写建议落地要多少成本。MyBatis 的落地路径最短按语句标识找到文件、改写、评审、发布全程不碰 Java 代码不影响编译产物的其他部分。JDBC 路线相似语句在代码里改语句等于改代码多一次编译但少一层查找。JPA 的落地成本呈双峰分布如果优化手段在框架词汇表内加抓取计划、改抓取策略、加只读标记改动同样很小如果优化手段超出词汇表改连接顺序、加优化器提示、改写为窗口函数那么这条查询要从派生查询降级为原生查询伴随结果装配的改写与回归测试——同一条优化建议在 MyBatis 里是十分钟的事在 JPA 里可能是十分钟也可能是两天取决于建议内容是否越过了框架的表达边界。四、开发效率省下的时间从哪里来又在哪里还回去一样板代码账本一张表从建表到可用要写多少行把「开发效率高」翻译成可以数的东西一张十二列的业务表从建表完成到暴露完整的单表增删改查与分页能力三个方案各需要人写多少行代码。模板化 JDBC 路线实体类约四十行含金额与时间类型的规范声明行映射器约二十行数据访问类的八个方法约一百二十行合计约一百八十行全部需要人工书写且逐方法测试。MyBatis 原生路线实体类四十行映射接口十五行映射文件约九十行合计约一百四十五行叠加增强层后单表部分由通用方法承接人写代码骤降到六十行以内。JPA 路线实体类六十行含关系与主键策略注解仓库接口五行合计约六十五行——分页、排序、按属性查询由框架派生几乎零增量。单表场景的结论清晰JPA 与「MyBatis 加增强层」在同一量级纯手写路线贵三倍。但这本账要往下翻一页样板代码是一次性成本而下面两项是持续性成本持续项才是效率账的大头。二模式演进时的修改面业务表加一个字段三条路线的波及范围完全不同。JPA实体类加一个带注解的属性一处修改编译器保证引用一致性自动建表校验仅限开发环境还能提示结构漂移。MyBatis实体属性、结果映射、涉及该列的每一条插入与更新语句三类位置需要人工同步编译器对映射文件里的列名拼写无能为力漏改的典型症状是「新字段永远是空值」且不报任何错误。模板化 JDBC实体、行映射器、每一条相关语句与 MyBatis 相当且同样没有编译期保护。模式变更频繁的系统快速迭代期的业务中台、字段随运营活动增减的营销系统这一项的权重应当放到最大——每月十次表结构变更的项目里JPA 一年省下的不是几百行代码是几十次「漏改导致的静默缺陷」的排查成本。反过来结构稳定的核心账务表一年改不了两次这项优势对它几乎不计分。三效率曲线的翻转点把项目周期拉长看三条路线的累计投入曲线会交叉。前三个月JPA 路线明显领先领域模型即数据模型改动集中样板近零。第四到第九个月曲线开始靠拢复杂列表页出现JPA 团队开始为抓取策略、分页语句形状、规范查询的可读性支付调试与评审成本MyBatis 团队的映射文件在膨胀但每一次支付都是线性的、可预期的。九个月之后的走向取决于一个变量JPA 团队有没有在第一次踩坑时建立起时机模型的团队共识与语句断言设施。建立了曲线继续平缓没建立每个新人都会把懒加载异常、级联误删、批量劣化重新踩一遍效率优势被排障时间逐月吃回。这条曲线解释了为什么关于 JPA 的口碑呈两极分布说它高效的团队与说它是坑的团队都没有说谎他们只是处在同一条曲线的不同分支上。选 JPA 选的不是一个更快的工具是一条「前期快、中期靠纪律守住」的路线选 MyBatis 选的是一条「全程匀速、成本可预期」的路线。风险偏好本身就是选型输入。五、复杂查询分野真正的位置一复杂查询的五种形态「复杂查询」在选型讨论里通常是一个模糊的整体拆开看是五种形态三个方案对它们的适配度各不相同。形态一宽连接读取——详情页一次取回主体与三四个关联块。这是 JPA 的主场抓取计划显式声明后一条语句解决对象装配免费MyBatis 需要手写连接与嵌套结果映射装配规则冗长JDBC 全手工。形态二固定形状的聚合报表——对账单、日报语句长而稳定。MyBatis 与 JDBC 直接书写JPA 走原生查询加结果投影三者都可承接差距不大。形态三动态谓词筛选——运营后台的多条件搜索谓词随入参增减。MyBatis 动态标签是为此而生JPA 的规范查询构造器能做但可读性差实践里常引入第三方查询构造库补位模板化 JDBC 需要手工拼接最易失控。形态四批量比对与集合运算——临时表、多步骤中间结果、跨库汇总。这类已经溢出「应用内查询」的范畴三个方案都只是执行通道真正的选择在于要不要下推到数据库存储过程或上收到数据仓库。形态五方言特性依赖——窗口函数、递归公用表表达式、JSON 列操作、全文检索。SQL 可见的方案直接书写JPA 只能原生查询且部分特性与实体装配的兼容性需要逐个验证。五种形态数下来结论比「复杂用 MyBatis」精细得多真正拉开差距的只有形态三——动态谓词是 MyBatis 相对纯 JPA 的决定性优势形态一反而是 JPA 的优势区其余三种形态里三个方案的差距远小于口碑差距。二JPA 的三条出路与各自代价JPA 面对超出派生查询能力的需求时有三条出路工程上应当按序取用。第一条是对象查询语言加显式抓取覆盖形态一与中等复杂度的形态二保持对象装配与类型安全应当是默认选择。第二条是规范查询构造器或第三方类型安全查询库覆盖形态三代价是代码量与可读性——纯规范构造器写一个五条件动态查询在六十行上下引入类型安全查询库后降到二十行且编译期校验列引用但这等于往技术栈里再加一个组件。第三条是原生查询加投影接口覆盖形态二与形态五拿回全部 SQL 能力代价是丧失可移植性、结果装配需要显式声明、语句散落在注解里不如映射文件集中。三条出路都通但要看到一个结构性事实当一个 JPA 项目里原生查询占比超过三成它实际上已经变成了一个「没有映射文件与动态标签的 MyBatis 项目」——此时正确的动作不是继续硬撑而是承认该模块的查询形状已经越界把它划出对象映射的辖区。三MyBatis 动态 SQL 的强项与失控点动态标签是 MyBatis 的护城河但护城河内也有暗礁评审时要盯三处。第一处是文本替换符${}出现在任何与用户输入有数据流连通的位置都是注入缺陷排序列名这类确实无法参数化的场景必须走白名单映射这条规则应当进静态扫描而不是靠人记。第二处是标签嵌套深度两层以内的条件标签可读性尚可三层嵌套加循环标签的映射文件实际上已经是一门用尖括号书写的编程语言评审有效性急剧下降——超过三层的动态语句应当拆分为多条具名语句由 Java 层做路由。第三处是隐式的全表扫描分支所有条件都为空时where标签会生成没有谓词的语句这是动态查询最经典的事故形状防线是在入口处强制至少一个必选条件或在映射层为空条件组合设置兜底限制。这三处暗礁有一个共同点都不是框架缺陷是「把语句形状的决定权交给运行期入参」这件事本身固有的风险。动态 SQL 的能力与风险是同一枚硬币选择 MyBatis 承接动态查询的团队等于同时选择了为这枚硬币建立评审与扫描纪律。六、性能框架差距小于默认值差距一性能差异的四个真实来源「JDBC 最快、JPA 最慢」是选型讨论里最常见也最粗糙的性能结论。在语句形状相同的前提下三个方案的每语句开销差距在微秒量级——反射装配、快照留存、代理包装的成本相对一次零点几毫秒的网络往返只是零头。生产环境里真实的性能差距来自四个结构性来源每一个都比「框架本身的开销」大一到三个数量级。来源一是语句条数对象导航的按行触发查询能把一次列表请求放大成两百条语句这是 JPA 误用的标志性事故但 MyBatis 的嵌套查询式关联映射同样会按行触发——两者的差别只在默认值JPA 的关联导航默认可用因此容易无意触发MyBatis 需要显式配置因此触发得更自觉。来源二是结果集形状多集合连接抓取的行数乘法、宽表全列返回、深度偏移分页三个方案里都能发生只是 JPA 把形状决定权交给了翻译层形状失控更难在代码里被看见。来源三是写路径合批JDBC 批处理接口与重写参数配置齐备时十万行插入是几十次往返JPA 需要合批开关、主键策略配合与分批清理三者同时正确才能达到同一量级任何一项缺失就退化为十万次往返MyBatis 的批执行器居中。来源四是上下文成本JPA 特有——托管实体的快照比较在长事务大上下文里是实打实的处理器与内存开销报表与批量场景必须用只读标记或无状态会话绕开。这四个来源给出的选型含义是性能不适合作为三选一的第一判据因为四个来源里有三个在任何方案下都需要同样的治理动作控制条数、控制形状、配置合批真正框架相关的只有上下文成本一项而它有成熟的绕开手段。用「哪个快」来选型大概率选了一个错误的问题。二公平对照实验怎么做如果组织仍然要求一份性能对比报告至少要守住四条实验纪律否则报告只是立场的数字化。第一语句形状对齐三个方案执行的最终语句必须逐条核对为相同形状比较「JPA 默认翻译」与「手写优化 SQL」得出的不是框架差距是熟练度差距。第二默认值调平JPA 侧打开合批与只读优化、MyBatis 侧对齐执行器类型、JDBC 侧配置批处理重写连接池参数三侧一致。第三度量往返而不只是耗时同时记录语句条数与网络往返数条数不同的两组耗时没有可比性。第四用基准测试框架并报告分布预热、多轮、报告百分位而不是平均值单次计时的对比在统计上不成立。按这四条纪律做完多数团队会得到一个朴素的结果三个方案在对齐后的吞吐差距小于百分之十五而各自默认配置下的差距可以达到十倍——这个结果本身就是最有价值的选型输入性能不由框架名字决定由团队对所选框架默认值的掌握程度决定。七、团队成本最贵的不是学习曲线是误用曲线一两条曲线评估团队成本时通行的做法是比较学习曲线JDBC 概念最少、MyBatis 加一层映射语义、JPA 的实体状态机与时机模型最陡。这个排序没有错但它度量的是「从零到能写」的成本而团队真正支付的大头在另一条曲线上——误用曲线从「能写」到「不再写出带病代码」之间的距离。两条曲线的形状很不一样。JDBC 学习曲线最平误用曲线却不短资源泄漏、异常吞没、类型转换错位、注入漏洞全部依赖书写者的自觉团队规模越大方差越大。MyBatis 两条曲线都居中且误用形态集中注入、深嵌套、空条件容易用静态扫描与评审清单收口。JPA 学习曲线最陡误用曲线更陡且误用的反馈是延迟的——懒加载异常在换了调用路径后才出现级联误删在数据对不上时才被发现批量劣化在数据量上来后才显形。延迟反馈是 JPA 团队成本的核心特征错误写法在写下的当天几乎总是能跑的。这给选型的启示是评估「团队会不会用 X」时正确的问题不是「多久能上手」而是「第一批误用出现时团队里有没有人能认出它们」。有经验者坐镇的团队JPA 的误用曲线被评审截断总成本可控没有的团队同样的曲线以生产事故的形式全额支付。二可评审性与知识分布团队成本还有一个静态维度代码合入前评审者能看懂多少、拦得住多少。MyBatis 的评审面是集中的映射文件加接口一个熟悉 SQL 的评审者不必熟悉 Java就能覆盖大部分风险面评审技能与数据库管理员的既有技能重合——这在数据库治理强势的组织里是实质性优势。JPA 的评审面是实体注解、方法签名与事务边界评审者必须同时掌握框架时机模型与业务上下文能胜任的人在多数团队里是个位数评审因此容易退化为形式。模板化 JDBC 的评审面最直白但最分散风险藏在每一处手写的资源与类型处理里靠评审拦截的效率最低更适合靠封装收口——把资源管理压进模板让评审只需要看语句与映射函数。知识分布同样值得写进选型依据招聘市场上会写 SQL 的候选人远多于真正理解对象映射时机模型的候选人国内市场 MyBatis 的存量项目与熟练者基数大跨国团队与新一代云原生项目里 JPA 加仓库抽象是默认栈。选型选的也是未来五年你要持续招到的人的画像——这条约束经常比任何技术论据都硬。八、选型判据把四个维度合成一条决策路径一六类典型场景的直接判定四本账记完多数真实项目可以对号入座到六类场景里每类给出直接判定与一条注意事项。1、标准业务中台以 JPA 为主干表多、以聚合根为中心读写、模式演进频繁、复杂查询集中在少数列表页。判定JPA 加仓库抽象为主干列表页用原生查询或专门的查询模块承接。注意立项当天就建立语句日志核对与条数断言的习惯把误用曲线截断在第一个迭代里。2、运营与报表后台以 MyBatis 为主干动态筛选密集、语句形状由运营需求驱动、改语句的频率高于改模型。判定MyBatis 加增强层动态标签承接筛选映射文件按业务域拆分。注意${}扫描与空条件兜底进流水线映射文件的评审人绑定数据库管理员。3、账务与交易核心SQL 可见方案倾向 MyBatis 或模板化 JDBC语句必须逐条评审、锁语义必须显式、审计要求语句与代码双向可溯。判定MyBatis语句集中便于审计或模板化 JDBC依赖最少不建议对象映射——不是能力问题是「语句由运行期生成」与审计要求的结构性冲突。注意金额一律BigDecimal更新一律带乐观锁版本条件。4、高吞吐批处理与数据管道模板化 JDBC 或 MyBatis 批执行器单次任务百万行级、语句形状固定、对象身份无价值。判定模板化 JDBC 批处理接口为首选MyBatis 批执行器次之若系统主干是 JPA此模块单独降级不要在托管实体上硬跑。注意合批参数与主键策略在压测里验证往返数不要只看耗时。5、小团队快速验证JPA 或「MyBatis 加增强层」选熟不选优三五人、三个月出可用版本、表结构每周在变。判定团队排过障的那个就是对的那个两者在这个规模下的效率差小于熟练度差。注意无论选哪个把数据访问收进独立包给半年后可能的重构留出切口。6、多数据库产品化交付JPA 的少数硬优势区同一套代码要跑在客户各异的数据库上。判定JPA 的方言层是三个方案里唯一系统性解决可移植性的此需求下它几乎是唯一解。注意方言覆盖不等于行为一致分页、锁与函数差异仍需逐库回归。二混用策略与边界纪律按模块混用是大型系统的常态而不是妥协但混用只有在三条纪律下才不劣化为混乱。第一条边界画在模块上不画在类上。一个业务模块内只出现一种数据访问技术混用发生在模块之间同一个服务类里既有仓库注入又有映射器注入是边界失守的第一信号。第二条写路径不共享。同一张表的写入只允许从一条技术路线进入——JPA 的托管实体与旁路更新语句并存时一级缓存与快照会读到旧值这类问题的排查成本足以抵消混用的全部收益读路径可以共享但要接受快照口径可能不同。第三条用依赖规则固定边界。在构建配置里限制各模块可见的依赖让「报表模块引不到实体包、核心模块引不到映射器包」成为编译期事实而不是文档里的约定。三选错之后怎么换迁移成本清单选型判断应当包含「错了怎么办」的价格。三条常见迁移路径的成本形状JPA 迁往 MyBatis——语句要从翻译产物变成手写文本工作量大但机械风险集中在遗漏隐式行为级联、孤儿移除、乐观锁自动递增上这些行为在 JPA 里不占代码行数迁移时最容易漏。MyBatis 迁往 JPA——表结构要先补齐对象映射的前提代理键、外键关系明确存量动态查询基本无法自动转换实际工程里通常演化为「新模块 JPA、存量 MyBatis」的长期混用而不是替换。任一方迁往模板化 JDBC——语句层几乎无损损失全在装配与样板上适合作为局部模块的降级手段而不是全局迁移。三条路径的共同规律数据访问技术的替换成本与其抽象高度成正比且主要成本不在写新代码在于识别旧框架替你默默做掉的那些事。这条规律反过来就是一开始的架构建议把数据访问收进独立模块、禁止实体与会话对象泄漏到业务层——这不是为了迁移而设计是让「选错」从一次重写降级为一次替换。九、把选型写进工程防线与规则一三道防线选型结论如果只存在于会议纪要里半年后就会被新成员的习惯稀释。把它固化需要三道防线每道防线都要可检查。1、决策记录防线用架构决策记录把选型的输入第一章的六个数、结论各模块的技术路线与放弃项考虑过什么、为什么不选写成受版本管理的文档。它的价值不在当下在一年后有人提议「换个框架」时团队可以对着当年的输入检查哪些条件变了——条件没变就不必重开战场条件变了就按新输入重算而不是重新表决。2、静态检查防线把纪律翻译成流水线里的机器规则人只处理例外。1.1、注入面规则映射文件中的${}一律告警白名单场景显式豁免并注明理由拼接 SQL 的字符串操作在数据访问包外一律禁止。1.2、边界规则按构建模块限制依赖可见性实体包、映射器包、模板类分别只对授权模块开放业务层出现会话、实体管理器或结果集类型即告警。1.3、规范默认值规则JPA 侧关联注解必须显式声明抓取策略禁止依赖默认值批量任务类必须出现分批清理调用。MyBatis 侧动态语句必须有空条件兜底映射文件行数超过阈值告警促拆分。1.4、类型规则金额列的映射必须是BigDecimal时间列必须是不含时区歧义的类型由实体与结果映射的扫描规则兜底。3、测试闸门防线为关键路径建立语句断言测试用代理数据源捕获一次接口调用发出的全部语句断言条数上界与关键语句形状。这是唯一能同时约束三个方案的手段——JPA 的翻译漂移、MyBatis 的动态分支遗漏、JDBC 的手写回归都会在条数或形状上留下痕迹。每次框架升级、每次映射改动闸门自动重验选型时承诺的「可控」由此从形容词变成断言。二落地次序三道防线不必一次建齐。第一周做决策记录与依赖规则——两者都只需要写文档与改构建配置不碰业务代码第一个月内把注入面与类型规则接入流水线——规则现成接入成本以小时计语句断言测试从最重要的三个接口开始随事故与评审发现逐步扩面不要试图一次覆盖全部。次序的原则是先固化边界再固化写法最后固化行为——边界错了写法白搭写法错了行为必然漂移。结语回到标题写得出 SQL 不等于管得住 SQL。JDBC、MyBatis 与 JPA 的差别从来不是「会不会生成 SQL」的能力差别而是 SQL 的书写权、可见权与修改权被安放在哪一层的架构差别。JDBC 把三权全部留给每一个书写者代价是纪律靠人自觉MyBatis 把语句留给人、把机械劳动交给框架代价是映射与语句的同步靠人盯守JPA 把三权连同发出时机一并收走换来模式演进期的高效率代价是团队必须真正掌握它的时机模型并用测试设施重建可见性。三条路线没有全局最优解只有对给定的读写形状、变更频率、审计约束与团队画像的局部最优解。所以这次选型的正确答案不是一个名字而是一套动作先量出六个数再按模块对号入座然后用依赖规则把边界固化、用静态检查把纪律固化、用语句断言把行为固化。做完这套动作选 JDBC、MyBatis 还是 JPA 的争论会自然平息——因为无论名字是哪个SQL 都被管住了而管不住的团队换任何名字都只是换一种失控的方式。参考资料Oracle 官方 JDBC 基础教程Java Tutorials: JDBC BasicsJSR 221JDBC 4.0 规范提案页MyBatis 3 官方文档中文MyBatis 3 动态 SQL 官方文档MyBatis-Plus 官方文档Jakarta Persistence 3.1 规范Hibernate ORM 官方用户指南Spring Data JPA 官方参考文档Spring Framework 数据访问JdbcTemplate 与 JdbcClient官方文档Spring Data JDBC 项目主页Vlad Mihalcea高性能 Java 持久化技术博客Martin FowlerOrmHate——对象关系映射之争的冷静分析Martin FowlerData Mapper 模式企业应用架构模式目录Ted NewardThe Vietnam of Computer Science——对象关系映射的经典批评jOOQ 官方站点类型安全 SQL 构造的另一条路线jOOQ 博客关于 SQL 与 ORM 分工的系列讨论QueryDSL 官方站点HikariCP 连接池项目主页datasource-proxy语句捕获与断言的代理数据源p6spySQL 拦截与审计工具Flyway 数据库迁移工具文档JMHOpenJDK 官方微基准测试框架BaeldungJPA 与 JDBC 的差异对比BaeldungMyBatis 快速指南