专栏《Java高级进阶之路》从CRUD到AI工程师的完整跃迁路径Day 24/ 90主题回顾21年ORM发展史技术选型决策矩阵混合使用策略复杂查询JPA复杂报表MyBatis2019年接收过一个金融系统的维护项目。代码库里JDBC、Hibernate、MyBatis、Spring Data JPA四种持久层方案同时存在各自为战。一个简单的用户查询有人写JDBC直连有人写HQL有人用Mapper XML有人写JpaRepository——新人改个需求得先猜该在哪层动手。为什么会有这么多持久层方案它们分别解决了什么时代的问题今天这篇文章把过去21年Java持久层的四次进化讲清楚让你下次面对技术选型时不只是跟着团队用而是真正理解每种方案为什么存在、为什么消亡、为什么共存。第一次进化JDBC——从零到一的野蛮生长1997-2004JDBCJava Database Connectivity是Java持久层的起点。1997年随JDK 1.1发布让Java程序第一次能跟数据库对话。那个年代做数据库操作的代码看起来是这样的// JDBC 1.0 风格的原始数据访问2000年前后的典型写法 // 依赖JDK 1.1自带无需额外依赖 public class JdbcUserDao { // 数据库连接参数硬编码——那个年代没有配置中心的概念 private static final String URL jdbc:mysql://localhost:3306/myapp; private static final String USER root; private static final String PASSWORD 123456; public User findById(Long id) { Connection conn null; PreparedStatement stmt null; ResultSet rs null; try { // 1. 加载驱动——Class.forName是那个时代的标志性写法 Class.forName(com.mysql.cj.jdbc.Driver); // 2. 获取连接——每次操作都新建连接连接池概念还未普及 conn DriverManager.getConnection(URL, USER, PASSWORD); // 3. 创建预编译语句——JDBC 2.0引入PreparedStatement防SQL注入 stmt conn.prepareStatement(SELECT id, name, email, created_at FROM t_user WHERE id ?); stmt.setLong(1, id); // 4. 执行查询 rs stmt.executeQuery(); // 5. 手动映射结果集到Java对象 if (rs.next()) { User user new User(); user.setId(rs.getLong(id)); user.setName(rs.getString(name)); user.setEmail(rs.getString(email)); user.setCreatedAt(rs.getTimestamp(created_at).toLocalDateTime()); return user; } return null; } catch (Exception e) { throw new RuntimeException(数据库查询失败, e); } finally { // 6. 手动关闭资源——顺序不能错每个close都可能抛异常 // 三个try-catch嵌套是那个年代Java代码的标志性括号地狱 try { if (rs ! null) rs.close(); } catch (SQLException e) { /* 忽略 */ } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { /* 忽略 */ } try { if (conn ! null) conn.close(); } catch (SQLException e) { /* 忽略 */ } } } }这是30行代码做了查一个用户这么一件事。其中只有3行跟业务有关——那行SQL和两个set方法——其余全是样板代码加载驱动、打开连接、关闭连接、捕获异常。JDBC时代的核心矛盾Java是面向对象的语言关系型数据库是面向表和行的世界。它们之间的阻抗不匹配impedance mismatch是原生JDBC最大的痛点。一个User对象有name和email一个订单对象有对User的引用——但在数据库里你需要把这条引用翻译成user_id外键和JOIN语句。这个翻译过程每一行代码都是手动写的。JDBC留下了什么尽管写法原始JDBC确立了两个至今仍在用的核心抽象——Connection代表数据库连接和PreparedStatement代表预编译的SQL。后来的所有ORM框架底层都在用这两个类。理解了它们你就理解了Java持久层的基石。第二次进化Hibernate——全自动ORM的黄金时代2001-20102001年一个叫Gavin King的澳大利亚程序员受够了EJB 2.0的CMPContainer-Managed Persistence决定自己写一个轻量级的ORM框架。他命名它为Hibernate——冬眠寓意让Java对象在数据库中休眠。Hibernate带来了一个革命性的想法程序员不再写SQL只需要操作Java对象Hibernate自动把对象的变化同步到数据库。// Hibernate 5.x 时代的典型写法 // 依赖org.hibernate:hibernate-core:5.6.0.Final // Entity 标记这个类与数据库表 t_user 有映射关系 Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name name, length 100) private String name; Column(name email, length 200) private String email; // Hibernate的魔法在这里OneToMany让对象引用自动变成JOIN查询 OneToMany(mappedBy user, cascade CascadeType.ALL, fetch FetchType.LAZY) private ListOrder orders; // getter/setter 省略 } // 使用Hibernate操作数据——没有一行SQL public class HibernateUserDao { private SessionFactory sessionFactory; public User findById(Long id) { Session session sessionFactory.openSession(); try { // HQLHibernate Query Language面向对象的查询语言 // 下面这行HQL操作的是Java对象名User不是表名t_user User user session.createQuery( FROM User u LEFT JOIN FETCH u.orders WHERE u.id :id, User.class) .setParameter(id, id) .uniqueResult(); return user; } finally { session.close(); } } public void save(User user) { Session session sessionFactory.openSession(); Transaction tx session.beginTransaction(); try { // session.save(user) 一行代码Hibernate做了 // 1. 检查user是否已在持久化上下文中 // 2. 如果不脏生成INSERT语句 // 3. 级联保存关联的Order对象 // 4. 处理主键生成策略 session.save(user); tx.commit(); } catch (Exception e) { tx.rollback(); throw e; } finally { session.close(); } } }Hibernate的辉煌是真实的。它解决了JDBC时代的三大痛点对象关系映射Entity/Table/Column/OneToMany标注让对象和表自动映射不再手写ResultSet.getXxx()会话管理Session封装了连接管理、事务边界、一级缓存你不再手动管理Connection的生命周期延迟加载FetchType.LAZY让关联数据在真正使用时才加载不用一次性JOIN所有表但Hibernate也埋下了隐患。全自动ORM最大的问题是魔法太多——你写的代码少但Hibernate替你生成的SQL你可能根本没见过。当业务复杂到需要多表联合、分组聚合、子查询嵌套时HQL生成的SQL往往非常糟糕。这个现象有个著名的说法Hibernate让你快速完成80%的工作但剩下20%的工作会花掉你80%的时间。2010年前后Hibernate太重了成为了Java社区的共识。程序员们怀念JDBC时代对SQL的控制权但又不愿意回到手动管理连接的地狱。市场需要一个新的方案——既要SQL的控制权又要框架的便利性。第三次进化MyBatis——SQL主权的回归2010-2015MyBatis的前身叫iBATIS2002年由Clinton Begin创建。这个名字的本意是互联网abatis鹿砦一种防御工事暗示它是抵御数据层混乱的防线。MyBatis的核心哲学跟Hibernate完全相反不要替我写SQL让我自己写SQL你帮我做好映射就行。!-- MyBatis Mapper XML —— 开发者的SQL主权宣言 -- !-- 这是 mapper/UserMapper.xml -- ?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.UserMapper !-- MyBatis的ResultMap声明式的结果集映射 -- !-- 你把SELECT的列名跟Java属性名的对应关系写清楚剩下的交给MyBatis -- resultMap idUserResultMap typecom.example.entity.User id propertyid columnid/ result propertyname columnname/ result propertyemail columnemail/ result propertycreatedAt columncreated_at/ !-- MyBatis不替你自动JOIN但你可以在SQL里写然后用collection收集子对象 -- collection propertyorders columnid selectcom.example.mapper.OrderMapper.findByUserId/ /resultMap !-- SQL完全由你掌控——这是MyBatis跟Hibernate最根本的区别 -- select idfindById resultMapUserResultMap SELECT id, name, email, created_at FROM t_user WHERE id #{id} /select !-- 复杂查询写SQL天经地义MyBatis不拦你 -- select idfindTopUsersByOrderCount resultTypecom.example.dto.UserOrderStat SELECT u.id, u.name, COUNT(o.id) AS order_count, SUM(o.amount) AS total_amount FROM t_user u LEFT JOIN t_order o ON o.user_id u.id WHERE o.created_at #{startDate} GROUP BY u.id, u.name HAVING COUNT(o.id) #{minOrderCount} ORDER BY total_amount DESC LIMIT #{limit} /select /mapperMyBatis的成功不是技术上的突破而是哲学上的回归。它承认了一个事实SQL是经过数十年工业验证的数据操作语言试图用面向对象的方式替换它是徒劳的。程序员应该继续写SQL框架应该负责处理JDBC的样板代码和结果映射。在MyBatis如日中天的2010年代中国互联网公司几乎一边倒地选择了它。原因很实际互联网业务的查询逻辑极其复杂多变——分页、排序、多条件筛选、统计聚合、跨表关联——这些场景下自己写SQL远比让Hibernate生成SQL可控。但MyBatis也有代价。简单CRUD场景下每张表都要写Mapper XML和对应的SQL即便查所有和按ID查也只是SELECT语句的微小变体。于是MyBatis-Plus应运而生——给MyBatis套了一层代码生成的壳自动生成基础CRUD的SQL复杂查询继续手写。第四次进化JPA Spring Data——标准化的力量2015至今JPAJava Persistence API的诞生背景很微妙。它是Java EE社区对Hibernate巨大成功的一种回应——既然Hibernate证明了ORM的价值那我们应该制定一个标准让不同的ORM实现可以互换。JPA本质上是一套接口规范Hibernate是它最主流的实现。但JPASpring Data的组合带来了超越Hibernate的体验// Spring Data JPA —— 声明式查询的极致表达 // 依赖org.springframework.boot:spring-boot-starter-data-jpa // 内含Hibernate 5.x作为JPA实现 Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; private String email; Column(name created_at) private LocalDateTime createdAt; // 关联关系可以用JPA标准的OneToMany也可以完全不用 // 这是JPA比纯Hibernate灵活的地方——不强求你全自动ORM } // Repository接口——Spring Data时代你只需要定义接口 // Spring Data在运行时自动生成实现类包含CRUD全能力 public interface UserRepository extends JpaRepositoryUser, Long { // 方法命名即查询——Spring Data 解析方法名自动生成SQL // findByEmail 会被解析为: SELECT ... FROM t_user WHERE email ? OptionalUser findByEmail(String email); // 日期范围查询也是方法名搞定 ListUser findByCreatedAtBetween(LocalDateTime start, LocalDateTime end); // 当方法名不够时用Query直接回退到JPQL——JPQL是HQL的标准化版本 Query(SELECT u FROM User u WHERE u.email LIKE %:domain) ListUser findByEmailDomain(Param(domain) String domain); // 甚至可以直接回退到原生SQL——JPA允许你在需要时重获SQL控制权 Query(value SELECT * FROM t_user WHERE id :id LIMIT :limit, nativeQuery true) ListUser findByIdGreaterThanNative(Param(id) Long id, Param(limit) int limit); }JPASpring Data解决了MyBatis时代的简单CRUD也写SQL问题。你定义一个接口继承JpaRepositorySpring Data就自动提供save/findById/findAll/delete等方法。对于80%的日常CRUD操作你一行SQL都不用写。更重要的是Spring Data引入了一个关键的设计模式——Repository抽象层。它不关心底层是JPA还是MongoDB还是Redis对上层业务代码来说从某处获取数据这个操作是统一的接口。现代方案混合策略——各取所长到这里你会发现一个规律每一代框架都在纠正上一代的极端。维度JDBCHibernateMyBatisJPASpring DataSQL控制权100%0%自动生成100%手写可切换自动/手写样板代码量极高极低中等极低复杂查询能力强但累弱HQL局限强中nativeQuery回退学习曲线低高中中适用场景已淘汰遗留系统报表/BI系统领域CRUD所以今天的最佳实践不是选一个而是混合使用业务分层 领域层Domain Layer │ ├── 增删改 简单按ID/条件查询 ──→ Spring Data JPA │ 场景用户注册、修改资料、分页列表 │ 优势零SQL方法名即查询开发效率最高 │ ├── 复杂统计报表 多表关联查询 ──→ MyBatis-Plus 手写SQL │ 场景月度经营分析、用户行为漏斗、库存周转率 │ 优势SQL完全可控DBA可以优化执行计划 │ └── 全文搜索 模糊匹配 ──→ Elasticsearch 场景商品搜索、日志检索、语义搜索 优势倒排索引毫秒级响应// 混合策略的Spring Boot配置示例 // 依赖版本Spring Boot 3.2 MyBatis-Plus 3.5.5 Spring Data JPA Configuration EnableTransactionManagement public class PersistenceConfig { // JPA 数据源主库负责业务CRUD Primary Bean(name jpaDataSource) ConfigurationProperties(prefix spring.datasource.jpa) public DataSource jpaDataSource() { return DataSourceBuilder.create().build(); } // JPA的EntityManager工厂绑定jpaDataSource Primary Bean(name entityManagerFactory) public LocalContainerEntityManagerFactoryBean entityManagerFactory( EntityManagerFactoryBuilder builder, Qualifier(jpaDataSource) DataSource dataSource) { return builder .dataSource(dataSource) .packages(com.example.domain) // JPA实体在domain包 .build(); } // JPA事务管理器 Primary Bean(name jpaTransactionManager) public PlatformTransactionManager jpaTransactionManager( Qualifier(entityManagerFactory) EntityManagerFactory emf) { return new JpaTransactionManager(emf); } // MyBatis 数据源报表库负责复杂查询 Bean(name mybatisDataSource) ConfigurationProperties(prefix spring.datasource.mybatis) public DataSource mybatisDataSource() { return DataSourceBuilder.create().build(); } // MyBatis-Plus的SqlSessionFactory绑定mybatisDataSource Bean(name mybatisSqlSessionFactory) public MybatisSqlSessionFactoryBean mybatisSqlSessionFactory( Qualifier(mybatisDataSource) DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); // 复杂查询的Mapper在report包 factory.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/report/*.xml)); return factory; } // MyBatis事务管理器 Bean(name mybatisTransactionManager) public PlatformTransactionManager mybatisTransactionManager( Qualifier(mybatisDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } } // 业务层使用示例 Service public class UserReportService { // JPA Repository处理简单CRUD private final UserRepository userRepository; // MyBatis-Plus Mapper处理复杂报表 private final UserReportMapper userReportMapper; public UserReportService(UserRepository userRepository, UserReportMapper userReportMapper) { this.userRepository userRepository; this.userReportMapper userReportMapper; } // 简单查询直接用JPA public User getUserProfile(Long userId) { return userRepository.findById(userId) .orElseThrow(() - new RuntimeException(用户不存在)); } // 复杂统计走MyBatis的自定义SQL // 这个查询涉及5张表JOIN 窗口函数 条件聚合 // JPA的JPQL根本写不出来MyBatis的原生SQL正好胜任 public ListUserMonthlyReport getMonthlyReport(LocalDate month) { return userReportMapper.getMonthlyUserReport(month); } }AI时代持久层的下一个路口如果把这个进化脉络拉长来看每一代持久层框架解决的都是同一个问题——人类不应该把时间花在重复的数据搬运代码上。JDBC时代你搬运ResultSet到Java对象。Hibernate时代你搬运注解和配置。MyBatis时代你搬运SQL到XML。JPA时代你搬运方法名到Repository。AI正在改变这个等式。今天你描述我要查上个月买过手机、且总消费超过5000的用户AI可以直接生成对应的JPA Query或MyBatis SQL。这改变了传统持久层选型的逻辑——以前你选MyBatis是因为需要手写SQL选JPA是因为不想写SQL。但AI来了以后SQL由AI生成、你审核框架的选择更多取决于团队习惯和系统架构而不是谁写SQL更方便。建议新项目默认用JPAMyBatis混合策略。JPA负责80%的简单CRUD开发快MyBatis负责20%的复杂查询SQL可控。不需要纠结全选哪一个——它们是互补关系不是互斥关系。如果你接手了纯Hibernate的老项目不要急着重写。Hibernate到今天仍然是可用的ORM。只要做好两点——(a) 打开SQL日志确保你看到Hibernate生成的SQL(b) 复杂查询用nativeQuery回退到原生SQL——就足够应付绝大多数场景。不要为了统一而统一。我看到过很多团队为了技术栈统一把MyBatis的复杂SQL改成JPA的JPQL结果SQL从20行变成80行性能还下降了。尊重每种工具的设计哲学在实践中各取所长才是成熟工程师的做法。金句框架进化的本质不是谁厉害而是谁在正确的时间解决了正确的问题。理解了每一代框架为什么诞生、为什么消亡你才能真正做出适合自己的技术选型。下篇预告Day 25《分布式事务解决方案演进从XA二阶段到Seata AT模式》——当单体架构裂变成微服务一个订单要同时操作三个数据库事务的一致性怎么保证四种方案五种模式一次讲透。
Day24 JDBC、Hibernate、MyBatis、JPA持久层技术的四次进化
发布时间:2026/7/17 20:51:10
专栏《Java高级进阶之路》从CRUD到AI工程师的完整跃迁路径Day 24/ 90主题回顾21年ORM发展史技术选型决策矩阵混合使用策略复杂查询JPA复杂报表MyBatis2019年接收过一个金融系统的维护项目。代码库里JDBC、Hibernate、MyBatis、Spring Data JPA四种持久层方案同时存在各自为战。一个简单的用户查询有人写JDBC直连有人写HQL有人用Mapper XML有人写JpaRepository——新人改个需求得先猜该在哪层动手。为什么会有这么多持久层方案它们分别解决了什么时代的问题今天这篇文章把过去21年Java持久层的四次进化讲清楚让你下次面对技术选型时不只是跟着团队用而是真正理解每种方案为什么存在、为什么消亡、为什么共存。第一次进化JDBC——从零到一的野蛮生长1997-2004JDBCJava Database Connectivity是Java持久层的起点。1997年随JDK 1.1发布让Java程序第一次能跟数据库对话。那个年代做数据库操作的代码看起来是这样的// JDBC 1.0 风格的原始数据访问2000年前后的典型写法 // 依赖JDK 1.1自带无需额外依赖 public class JdbcUserDao { // 数据库连接参数硬编码——那个年代没有配置中心的概念 private static final String URL jdbc:mysql://localhost:3306/myapp; private static final String USER root; private static final String PASSWORD 123456; public User findById(Long id) { Connection conn null; PreparedStatement stmt null; ResultSet rs null; try { // 1. 加载驱动——Class.forName是那个时代的标志性写法 Class.forName(com.mysql.cj.jdbc.Driver); // 2. 获取连接——每次操作都新建连接连接池概念还未普及 conn DriverManager.getConnection(URL, USER, PASSWORD); // 3. 创建预编译语句——JDBC 2.0引入PreparedStatement防SQL注入 stmt conn.prepareStatement(SELECT id, name, email, created_at FROM t_user WHERE id ?); stmt.setLong(1, id); // 4. 执行查询 rs stmt.executeQuery(); // 5. 手动映射结果集到Java对象 if (rs.next()) { User user new User(); user.setId(rs.getLong(id)); user.setName(rs.getString(name)); user.setEmail(rs.getString(email)); user.setCreatedAt(rs.getTimestamp(created_at).toLocalDateTime()); return user; } return null; } catch (Exception e) { throw new RuntimeException(数据库查询失败, e); } finally { // 6. 手动关闭资源——顺序不能错每个close都可能抛异常 // 三个try-catch嵌套是那个年代Java代码的标志性括号地狱 try { if (rs ! null) rs.close(); } catch (SQLException e) { /* 忽略 */ } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { /* 忽略 */ } try { if (conn ! null) conn.close(); } catch (SQLException e) { /* 忽略 */ } } } }这是30行代码做了查一个用户这么一件事。其中只有3行跟业务有关——那行SQL和两个set方法——其余全是样板代码加载驱动、打开连接、关闭连接、捕获异常。JDBC时代的核心矛盾Java是面向对象的语言关系型数据库是面向表和行的世界。它们之间的阻抗不匹配impedance mismatch是原生JDBC最大的痛点。一个User对象有name和email一个订单对象有对User的引用——但在数据库里你需要把这条引用翻译成user_id外键和JOIN语句。这个翻译过程每一行代码都是手动写的。JDBC留下了什么尽管写法原始JDBC确立了两个至今仍在用的核心抽象——Connection代表数据库连接和PreparedStatement代表预编译的SQL。后来的所有ORM框架底层都在用这两个类。理解了它们你就理解了Java持久层的基石。第二次进化Hibernate——全自动ORM的黄金时代2001-20102001年一个叫Gavin King的澳大利亚程序员受够了EJB 2.0的CMPContainer-Managed Persistence决定自己写一个轻量级的ORM框架。他命名它为Hibernate——冬眠寓意让Java对象在数据库中休眠。Hibernate带来了一个革命性的想法程序员不再写SQL只需要操作Java对象Hibernate自动把对象的变化同步到数据库。// Hibernate 5.x 时代的典型写法 // 依赖org.hibernate:hibernate-core:5.6.0.Final // Entity 标记这个类与数据库表 t_user 有映射关系 Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name name, length 100) private String name; Column(name email, length 200) private String email; // Hibernate的魔法在这里OneToMany让对象引用自动变成JOIN查询 OneToMany(mappedBy user, cascade CascadeType.ALL, fetch FetchType.LAZY) private ListOrder orders; // getter/setter 省略 } // 使用Hibernate操作数据——没有一行SQL public class HibernateUserDao { private SessionFactory sessionFactory; public User findById(Long id) { Session session sessionFactory.openSession(); try { // HQLHibernate Query Language面向对象的查询语言 // 下面这行HQL操作的是Java对象名User不是表名t_user User user session.createQuery( FROM User u LEFT JOIN FETCH u.orders WHERE u.id :id, User.class) .setParameter(id, id) .uniqueResult(); return user; } finally { session.close(); } } public void save(User user) { Session session sessionFactory.openSession(); Transaction tx session.beginTransaction(); try { // session.save(user) 一行代码Hibernate做了 // 1. 检查user是否已在持久化上下文中 // 2. 如果不脏生成INSERT语句 // 3. 级联保存关联的Order对象 // 4. 处理主键生成策略 session.save(user); tx.commit(); } catch (Exception e) { tx.rollback(); throw e; } finally { session.close(); } } }Hibernate的辉煌是真实的。它解决了JDBC时代的三大痛点对象关系映射Entity/Table/Column/OneToMany标注让对象和表自动映射不再手写ResultSet.getXxx()会话管理Session封装了连接管理、事务边界、一级缓存你不再手动管理Connection的生命周期延迟加载FetchType.LAZY让关联数据在真正使用时才加载不用一次性JOIN所有表但Hibernate也埋下了隐患。全自动ORM最大的问题是魔法太多——你写的代码少但Hibernate替你生成的SQL你可能根本没见过。当业务复杂到需要多表联合、分组聚合、子查询嵌套时HQL生成的SQL往往非常糟糕。这个现象有个著名的说法Hibernate让你快速完成80%的工作但剩下20%的工作会花掉你80%的时间。2010年前后Hibernate太重了成为了Java社区的共识。程序员们怀念JDBC时代对SQL的控制权但又不愿意回到手动管理连接的地狱。市场需要一个新的方案——既要SQL的控制权又要框架的便利性。第三次进化MyBatis——SQL主权的回归2010-2015MyBatis的前身叫iBATIS2002年由Clinton Begin创建。这个名字的本意是互联网abatis鹿砦一种防御工事暗示它是抵御数据层混乱的防线。MyBatis的核心哲学跟Hibernate完全相反不要替我写SQL让我自己写SQL你帮我做好映射就行。!-- MyBatis Mapper XML —— 开发者的SQL主权宣言 -- !-- 这是 mapper/UserMapper.xml -- ?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.UserMapper !-- MyBatis的ResultMap声明式的结果集映射 -- !-- 你把SELECT的列名跟Java属性名的对应关系写清楚剩下的交给MyBatis -- resultMap idUserResultMap typecom.example.entity.User id propertyid columnid/ result propertyname columnname/ result propertyemail columnemail/ result propertycreatedAt columncreated_at/ !-- MyBatis不替你自动JOIN但你可以在SQL里写然后用collection收集子对象 -- collection propertyorders columnid selectcom.example.mapper.OrderMapper.findByUserId/ /resultMap !-- SQL完全由你掌控——这是MyBatis跟Hibernate最根本的区别 -- select idfindById resultMapUserResultMap SELECT id, name, email, created_at FROM t_user WHERE id #{id} /select !-- 复杂查询写SQL天经地义MyBatis不拦你 -- select idfindTopUsersByOrderCount resultTypecom.example.dto.UserOrderStat SELECT u.id, u.name, COUNT(o.id) AS order_count, SUM(o.amount) AS total_amount FROM t_user u LEFT JOIN t_order o ON o.user_id u.id WHERE o.created_at #{startDate} GROUP BY u.id, u.name HAVING COUNT(o.id) #{minOrderCount} ORDER BY total_amount DESC LIMIT #{limit} /select /mapperMyBatis的成功不是技术上的突破而是哲学上的回归。它承认了一个事实SQL是经过数十年工业验证的数据操作语言试图用面向对象的方式替换它是徒劳的。程序员应该继续写SQL框架应该负责处理JDBC的样板代码和结果映射。在MyBatis如日中天的2010年代中国互联网公司几乎一边倒地选择了它。原因很实际互联网业务的查询逻辑极其复杂多变——分页、排序、多条件筛选、统计聚合、跨表关联——这些场景下自己写SQL远比让Hibernate生成SQL可控。但MyBatis也有代价。简单CRUD场景下每张表都要写Mapper XML和对应的SQL即便查所有和按ID查也只是SELECT语句的微小变体。于是MyBatis-Plus应运而生——给MyBatis套了一层代码生成的壳自动生成基础CRUD的SQL复杂查询继续手写。第四次进化JPA Spring Data——标准化的力量2015至今JPAJava Persistence API的诞生背景很微妙。它是Java EE社区对Hibernate巨大成功的一种回应——既然Hibernate证明了ORM的价值那我们应该制定一个标准让不同的ORM实现可以互换。JPA本质上是一套接口规范Hibernate是它最主流的实现。但JPASpring Data的组合带来了超越Hibernate的体验// Spring Data JPA —— 声明式查询的极致表达 // 依赖org.springframework.boot:spring-boot-starter-data-jpa // 内含Hibernate 5.x作为JPA实现 Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; private String email; Column(name created_at) private LocalDateTime createdAt; // 关联关系可以用JPA标准的OneToMany也可以完全不用 // 这是JPA比纯Hibernate灵活的地方——不强求你全自动ORM } // Repository接口——Spring Data时代你只需要定义接口 // Spring Data在运行时自动生成实现类包含CRUD全能力 public interface UserRepository extends JpaRepositoryUser, Long { // 方法命名即查询——Spring Data 解析方法名自动生成SQL // findByEmail 会被解析为: SELECT ... FROM t_user WHERE email ? OptionalUser findByEmail(String email); // 日期范围查询也是方法名搞定 ListUser findByCreatedAtBetween(LocalDateTime start, LocalDateTime end); // 当方法名不够时用Query直接回退到JPQL——JPQL是HQL的标准化版本 Query(SELECT u FROM User u WHERE u.email LIKE %:domain) ListUser findByEmailDomain(Param(domain) String domain); // 甚至可以直接回退到原生SQL——JPA允许你在需要时重获SQL控制权 Query(value SELECT * FROM t_user WHERE id :id LIMIT :limit, nativeQuery true) ListUser findByIdGreaterThanNative(Param(id) Long id, Param(limit) int limit); }JPASpring Data解决了MyBatis时代的简单CRUD也写SQL问题。你定义一个接口继承JpaRepositorySpring Data就自动提供save/findById/findAll/delete等方法。对于80%的日常CRUD操作你一行SQL都不用写。更重要的是Spring Data引入了一个关键的设计模式——Repository抽象层。它不关心底层是JPA还是MongoDB还是Redis对上层业务代码来说从某处获取数据这个操作是统一的接口。现代方案混合策略——各取所长到这里你会发现一个规律每一代框架都在纠正上一代的极端。维度JDBCHibernateMyBatisJPASpring DataSQL控制权100%0%自动生成100%手写可切换自动/手写样板代码量极高极低中等极低复杂查询能力强但累弱HQL局限强中nativeQuery回退学习曲线低高中中适用场景已淘汰遗留系统报表/BI系统领域CRUD所以今天的最佳实践不是选一个而是混合使用业务分层 领域层Domain Layer │ ├── 增删改 简单按ID/条件查询 ──→ Spring Data JPA │ 场景用户注册、修改资料、分页列表 │ 优势零SQL方法名即查询开发效率最高 │ ├── 复杂统计报表 多表关联查询 ──→ MyBatis-Plus 手写SQL │ 场景月度经营分析、用户行为漏斗、库存周转率 │ 优势SQL完全可控DBA可以优化执行计划 │ └── 全文搜索 模糊匹配 ──→ Elasticsearch 场景商品搜索、日志检索、语义搜索 优势倒排索引毫秒级响应// 混合策略的Spring Boot配置示例 // 依赖版本Spring Boot 3.2 MyBatis-Plus 3.5.5 Spring Data JPA Configuration EnableTransactionManagement public class PersistenceConfig { // JPA 数据源主库负责业务CRUD Primary Bean(name jpaDataSource) ConfigurationProperties(prefix spring.datasource.jpa) public DataSource jpaDataSource() { return DataSourceBuilder.create().build(); } // JPA的EntityManager工厂绑定jpaDataSource Primary Bean(name entityManagerFactory) public LocalContainerEntityManagerFactoryBean entityManagerFactory( EntityManagerFactoryBuilder builder, Qualifier(jpaDataSource) DataSource dataSource) { return builder .dataSource(dataSource) .packages(com.example.domain) // JPA实体在domain包 .build(); } // JPA事务管理器 Primary Bean(name jpaTransactionManager) public PlatformTransactionManager jpaTransactionManager( Qualifier(entityManagerFactory) EntityManagerFactory emf) { return new JpaTransactionManager(emf); } // MyBatis 数据源报表库负责复杂查询 Bean(name mybatisDataSource) ConfigurationProperties(prefix spring.datasource.mybatis) public DataSource mybatisDataSource() { return DataSourceBuilder.create().build(); } // MyBatis-Plus的SqlSessionFactory绑定mybatisDataSource Bean(name mybatisSqlSessionFactory) public MybatisSqlSessionFactoryBean mybatisSqlSessionFactory( Qualifier(mybatisDataSource) DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); // 复杂查询的Mapper在report包 factory.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/report/*.xml)); return factory; } // MyBatis事务管理器 Bean(name mybatisTransactionManager) public PlatformTransactionManager mybatisTransactionManager( Qualifier(mybatisDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } } // 业务层使用示例 Service public class UserReportService { // JPA Repository处理简单CRUD private final UserRepository userRepository; // MyBatis-Plus Mapper处理复杂报表 private final UserReportMapper userReportMapper; public UserReportService(UserRepository userRepository, UserReportMapper userReportMapper) { this.userRepository userRepository; this.userReportMapper userReportMapper; } // 简单查询直接用JPA public User getUserProfile(Long userId) { return userRepository.findById(userId) .orElseThrow(() - new RuntimeException(用户不存在)); } // 复杂统计走MyBatis的自定义SQL // 这个查询涉及5张表JOIN 窗口函数 条件聚合 // JPA的JPQL根本写不出来MyBatis的原生SQL正好胜任 public ListUserMonthlyReport getMonthlyReport(LocalDate month) { return userReportMapper.getMonthlyUserReport(month); } }AI时代持久层的下一个路口如果把这个进化脉络拉长来看每一代持久层框架解决的都是同一个问题——人类不应该把时间花在重复的数据搬运代码上。JDBC时代你搬运ResultSet到Java对象。Hibernate时代你搬运注解和配置。MyBatis时代你搬运SQL到XML。JPA时代你搬运方法名到Repository。AI正在改变这个等式。今天你描述我要查上个月买过手机、且总消费超过5000的用户AI可以直接生成对应的JPA Query或MyBatis SQL。这改变了传统持久层选型的逻辑——以前你选MyBatis是因为需要手写SQL选JPA是因为不想写SQL。但AI来了以后SQL由AI生成、你审核框架的选择更多取决于团队习惯和系统架构而不是谁写SQL更方便。建议新项目默认用JPAMyBatis混合策略。JPA负责80%的简单CRUD开发快MyBatis负责20%的复杂查询SQL可控。不需要纠结全选哪一个——它们是互补关系不是互斥关系。如果你接手了纯Hibernate的老项目不要急着重写。Hibernate到今天仍然是可用的ORM。只要做好两点——(a) 打开SQL日志确保你看到Hibernate生成的SQL(b) 复杂查询用nativeQuery回退到原生SQL——就足够应付绝大多数场景。不要为了统一而统一。我看到过很多团队为了技术栈统一把MyBatis的复杂SQL改成JPA的JPQL结果SQL从20行变成80行性能还下降了。尊重每种工具的设计哲学在实践中各取所长才是成熟工程师的做法。金句框架进化的本质不是谁厉害而是谁在正确的时间解决了正确的问题。理解了每一代框架为什么诞生、为什么消亡你才能真正做出适合自己的技术选型。下篇预告Day 25《分布式事务解决方案演进从XA二阶段到Seata AT模式》——当单体架构裂变成微服务一个订单要同时操作三个数据库事务的一致性怎么保证四种方案五种模式一次讲透。