JDBC 到 ORM:Spring Data JDBC、JPA 与 MyBatis 的定位与选型

概述

衔接前文段落

JDBC 系列已经贯穿了从驱动到连接池的全部底层内核,并且第 10 篇教会了我们用反模式的眼光审视数据库访问。然而,现代开发已很少直接用 RowMapper 逐字段封装。从 JDBC 到 ORM,Java 数据访问建立了一条从手动控制到完全自动化的光谱,而每一种选择都自带其反模式陷阱。本文作为 JDBC 系列的收束之桥和后续 ORM 系列的起点,将帮助开发者理解这条光谱的设计逻辑,以及如何根据团队和业务的需求在 JdbcTemplateSpring Data JDBCJPAMyBatis 之间做出理智且安全的选型。

总结性引言

如果你的应用中仍然遍布 JdbcTemplate.query(sql, rowMapper),你是否该迁移到 MyBatis?如果你的项目大量使用 JPA,为何有些查询又慢得离谱,甚至 OSIV 正在悄悄拖垮连接池?Spring Data JDBC 这个"新秀"为什么没有 JPA 的一级缓存却反而更契合微服务?这些问题的答案,都根植于各框架对"对象关系阻抗不匹配"这件事的不同解决哲学。本文将深入这些框架的核心理念,对比其设计取舍,并提供一套可落地的场景化选型框架和反模式避险清单。

核心要点

  • 阻抗不匹配与映射哲学:从手动映射到完整 ORM 的谱系。
  • 四大框架核心理念JdbcTemplate 的控制,Spring Data JDBC 的聚合,JPA 的透明持久化与潜在陷阱,MyBatis 的显式 SQL。
  • 多维度对比与选型框架:控制力、效率、性能、DDD 契合度的权衡,并引入事务统一性维度。
  • 反模式预警:OSIV、N+1、JPA 意外更新、动态 SQL 滥用等,部分结合第10篇案例。

文章组织架构图

flowchart TB subgraph s1 ["1. 对象关系阻抗不匹配与映射谱系"] direction LR A1["面向对象与关系模型冲突"] --> A2["映射方案连续谱系"] end subgraph s2 ["2. JdbcTemplate:手动控制的极致"] end subgraph s3 ["3. Spring Data JDBC:聚合驱动的持久化"] end subgraph s4 ["4. Spring Data JPA / Hibernate:持久化上下文与自动映射"] end subgraph s5 ["5. MyBatis:显式 SQL 与半自动数据映射器"] end subgraph s5B ["5B. 四大技术的定位关系总览"] end subgraph s6 ["6. 四大技术多维度深度对比与 Spring 事务统一性"] end subgraph s7 ["7. 场景化选型决策框架"] end subgraph s8 ["8. 常见 ORM 反模式预警"] end subgraph s9 ["9. 面试高频专题"] end s1 --> s2 --> s3 --> s4 --> s5 --> s5B --> s6 --> s7 --> s8 --> s9

架构图说明

  • 总览说明 :全文共 10 个模块,在四大技术分别解析之后,增设了四大技术的定位关系总览模块,从纵向演进与横向互补两个维度讲清关系。随后进入多维度对比、选型决策和反模式预警,最后以面试题收尾。
  • 逐模块说明 :模块 1 建立问题基座;模块 2-5 分别是四大方案的独立解析;模块 5B 全新增加,强化关系阐述;模块 6-7 提供对比与决策框架,并融入事务统一性;模块 8 预警生产陷阱并关联系列反模式;模块 9 深化面试巩固。
  • 关键结论没有银弹。理解阻抗不匹配的本质以及各框架对它的回应,是技术选型时超越"谁火用谁"的关键。在全自动 ORM 与手写 SQL 之间,存在一条可以根据业务复杂度弹性调整的路线。

1. 对象关系阻抗不匹配与映射谱系

1.1 从 JDBC ResultSet 手动封装说起

回顾 JDBC 核心流程:获取 Connection → 创建 PreparedStatement → 执行查询得到 ResultSet → 遍历结果集,逐列取值,手动构造对象图。这一过程的本质是将关系模型 (表、行、列、外键)转换为对象模型 (类、对象、引用、集合)。在第 3 篇 JdbcTemplate 中,虽然资源管理实现了自动化,但映射仍由 RowMapper 完成,每一列到属性的赋值都需要显式编码。这种手工映射在业务复杂度上升时变得灾难化:关联查询的分层对象组装、嵌套对象的 N+1 次查询、更新时字段的新旧值比对,全部需要开发者自己控制。当 Order 对象包含 20 个属性和 3 个关联集合时,RowMapper 的编写将成为重复、脆弱且难以维护的代码泥潭。

1.2 面向对象与关系模型的五大冲突点

对象-关系阻抗不匹配(Object-Relational Impedance Mismatch)是 1990 年代面向对象编程兴起时被正式定义的术语,核心冲突包括:

  1. 继承 :对象模型通过 extendsimplements 实现 IS-A 关系,而关系表本身没有子类型概念,需要借助单表继承(所有子类存同一张表)、类表继承(父类一张表,子类各一张表用外键关联)或具体表继承(每个具体子类一张表)三种策略来模拟。无论哪种策略,都引入了额外的表管理复杂度。
  2. 关联 :对象通过引用(order.getCustomer())表达关联,关系模型通过外键和 JOIN 来表达。对象世界中的单向引用、双向关联在映射时必须明确表连接方向,且持久化时需要维护外键列的同步。
  3. 粒度 :对象可能拥有内嵌的值对象,如 Address 嵌入在 Customer 中,但关系表可能是平铺的列(customer 表包含 streetcityzip 列)或拆分为单独的 address 表。这种粒度差异使得"修改地址"这个动作在对象世界是一个简单的赋值,在数据库世界可能需要关联更新多列或多行。
  4. 身份 :Java 对象用 ==equals() 比较,数据库用主键值标识同一性。在同一个持久化上下文中,相同的数据库行必须对应同一个 Java 对象(否则会出现数据覆盖和并发问题),这引出了工作单元身份映射的核心概念。
  5. 导航 :对象图可双向遍历(order.getItems() / item.getOrder()),关系模型的关联查询取决于 JOIN 的子句方向。当需要支持双向导航时,ORM 必须决定是预加载全部关联(Eager)还是按需加载(Lazy),这直接影响性能和内存消耗。

1.3 对象模型与关系模型的结构差异

flowchart LR subgraph Object[面向对象模型] direction TB O_Order[Order
id, orderDate] O_Customer[Customer
id, name] O_Item[OrderItem
product, qty] O_Order -- "引用" --> O_Customer O_Order -- "集合" --> O_Item end subgraph Relational[关系模型] direction TB R_Order[orders 表
id, order_date, customer_id FK] R_Customer[customers 表
id, name] R_Item[order_items 表
id, order_id FK, product, qty] R_Order -- "FK" --> R_Customer R_Item -- "FK" --> R_Order end Object -- "继承/多态" --> Relational Object -- "对象引用 vs 外键" --> Relational Object -- "内嵌值对象 vs 平铺列" --> Relational Object -- "身份匹配 (equals vs PK)" --> Relational Object -- "双向导航 vs 单向JOIN" --> Relational

图1 说明(四层)

  • 图表意图:对比对象模型与关系模型的结构差异,用五个冲突方向标注核心矛盾。
  • 关键元素Order 对象通过引用指向 CustomerItem 集合,关系模型通过外键 customer_idorder_id 连接三张表。
  • 设计思想:对象世界以"聚合"为核心,数据库世界以"范式化"为核心,两种思维范式的距离就是所有映射技术的存在理由。
  • 对应正文:1.2 节展开的五大冲突在此图视觉化呈现,每个箭头对应一个冲突维度。

1.4 映射解决方案的连续谱系

Java 生态在应对阻抗不匹配的过程中,形成了从手动到自动的连续光谱,而非离散分类。Martin Fowler 《企业应用架构模式》定义了若干模式:

  • 事务脚本 + 表数据网关:直接写 SQL 执行,代表是早期 JDBC 或 .NET 的 DataTable。开发者完全掌握 SQL,但代码与数据库表结构强耦合。
  • 行数据入口:对象与一张表的一行一一对应,如 ActiveJDBC。每个对象自己负责与单表的 CRUD 交互,但无法处理对象之间的关系。
  • 活动记录(Active Record) :对象承载业务逻辑和持久化方法(如 order.save()),如 Ruby on Rails 的 ActiveRecord。对象知道自己如何持久化,数据库表与类严格一一对应,关系映射通过约定实现。
  • 数据映射器(Data Mapper) :完全分离对象和数据库访问,对象是纯 POJO,不包含任何持久化代码。映射器(如 MyBatis 的 ResultMap)负责将查询结果填充到对象中,对象对数据库的存在一无所知。
  • 完整 ORM(Full ORM) :不仅分离映射,还提供工作单元(Unit of Work)身份映射(Identity Map)透明持久化自动脏检查等机制,如 Hibernate/JPA。开发者只操作对象图,框架负责同步数据库。

Spring Data JDBC 的定位介于"活动记录"和"数据映射器"之间:实体本身不包含持久化方法(与活动记录相异),但通过 CrudRepository.save() 提供了面向聚合的持久化操作,底层是全量更新的数据映射。更精确地说,它是一种**聚合仓库模式(Aggregate Repository Pattern)**的实现,而非经典活动记录。

1.5 映射方案谱系图

flowchart TB A[手动映射
JDBC + RowMapper] --> B[半自动映射
JdbcTemplate 工具化] B --> C[显式数据映射器
MyBatis, SQL + 自动结果映射] C --> D[聚合仓库模式
Spring Data JDBC, 无缓存持久化] D --> E[完整 ORM
JPA/Hibernate, 持久化上下文]

图2 说明(四层)

  • 图表意图:展示从纯手动控制到全自动化的演进路径,修正了将 Spring Data JDBC 归类为活动记录的不精确表述。
  • 关键元素:每个节点代表一种映射哲学,箭头方向表示自动化程度增加,控制粒度减弱。
  • 设计思想:这不是优劣排名,而是一条权衡光谱。左边的方案对 SQL 的控制力更强,右边方案对对象的透明度更高。
  • 对应正文:1.4 节的理论总结,为后续四大技术定位奠定上下文,并准确安置 Spring Data JDBC 的位置。

2. JdbcTemplate:手动控制的极致

2.1 核心回顾与设计原理

JdbcTemplate 作为 Spring JDBC 模块的核心,在 JDBC 系列第 3 篇已被详细剖析。其本质是资源管理全自动 + 对象映射全手动 。模板方法模式使得连接获取、Statement 创建、结果集遍历的 try-catch-finally 样板代码完全消除,异常被统一转换为 Spring 的 DataAccessException 体系。然而,ResultSet 到领域对象的转换被完全暴露给开发者,由 RowMapper<T> 接口承接,开发者必须显式地处理每一列的索引、数据类型转换以及关联对象的嵌套。

正是这种极致的控制力,使得 JdbcTemplate 在以下场景中依然难以被替代:

  • 批量数据处理batchUpdate 直接执行预编译 SQL 批量操作,没有 ORM 的状态追踪开销。
  • 特定 SQL 优化 :需要用数据库特有的窗口函数、递归 CTE 或 HINT 时,手写 SQL 再经由 JdbcTemplate 执行是最直接的方式。
  • 极简 CRUD :表结构简单(例如 4-5 列),不需要任何对象关系映射,JdbcTemplate 的代码量反而比引入框架更少。
  • 排障与性能调优:当线上出现慢查询时,从框架生成的 SQL 中定位问题的难度远高于从代码中直接看到的显式 SQL。

2.2 代码示例:根据用户ID查询订单列表、创建订单

java 复制代码
// ---------- JdbcTemplate 实现 ----------
@Repository
public class JdbcOrderRepository {

    private final JdbcTemplate jdbcTemplate;

    public JdbcOrderRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * 查询用户订单及明细。
     * 设计要点:使用 LEFT JOIN 一次性获取主从数据,再按 orderId 分组聚合。
     * 这是 JDBC 下高效且唯一的做法,需要手动处理每一列的映射和分组聚合。
     */
    public List<Order> findByUserId(Long userId) {
        String sql = """
            SELECT o.id AS order_id, o.order_date, o.amount,
                   oi.id AS item_id, oi.product_name, oi.quantity
            FROM orders o
            LEFT JOIN order_items oi ON o.id = oi.order_id
            WHERE o.user_id = ?
            ORDER BY o.id
            """;
        // 用 LinkedHashMap 保持顺序和保证同一订单对象聚合
        Map<Long, Order> orderMap = new LinkedHashMap<>();
        jdbcTemplate.query(sql, (rs, rowNum) -> {
            Long orderId = rs.getLong("order_id");
            // computeIfAbsent:首次出现则创建 Order,后续行共享同一个对象
            Order order = orderMap.computeIfAbsent(orderId, id -> {
                try {
                    Order o = new Order();
                    o.setId(id);
                    o.setOrderDate(rs.getTimestamp("order_date").toLocalDateTime());
                    o.setAmount(rs.getBigDecimal("amount"));
                    o.setItems(new ArrayList<>());
                    return o;
                } catch (SQLException e) {
                    throw new DataAccessException("映射订单字段失败", e) {};
                }
            });
            // 处理订单明细,如果该行没有明细(item_id 为 null),跳过
            long itemId = rs.getLong("item_id");
            if (!rs.wasNull()) {
                OrderItem item = new OrderItem();
                item.setId(itemId);
                item.setProductName(rs.getString("product_name"));
                item.setQuantity(rs.getInt("quantity"));
                order.getItems().add(item);
            }
            return null; // 使用分组逻辑,RowMapper 的返回值在此处无意义
        }, userId);
        return new ArrayList<>(orderMap.values());
    }

    /**
     * 创建订单并回填自增主键。
     * 设计要点:使用 KeyHolder 获取数据库生成的订单 ID,然后批量插入明细。
     * 所有 SQL 必须显式编写,字段与占位符的顺序必须严格对应。
     */
    public Long createOrder(Order order) {
        String insertOrder = "INSERT INTO orders (user_id, order_date, amount) VALUES (?, ?, ?)";
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(insertOrder, new String[]{"id"});
            ps.setLong(1, order.getUserId());
            ps.setTimestamp(2, Timestamp.valueOf(order.getOrderDate()));
            ps.setBigDecimal(3, order.getAmount());
            return ps;
        }, keyHolder);
        Long orderId = keyHolder.getKey().longValue();

        // 插入明细 ------ 生产中应使用 batchUpdate 进一步优化
        String insertItem = "INSERT INTO order_items (order_id, product_name, quantity) VALUES (?, ?, ?)";
        for (OrderItem item : order.getItems()) {
            jdbcTemplate.update(insertItem, orderId, item.getProductName(), item.getQuantity());
        }
        return orderId;
    }
}

代码解读

  • 开发者拥有 100% 控制权,SQL、映射逻辑、主键策略全部由代码决定,没有任何黑盒。
  • 样板代码量巨大:关联对象的分组聚合逻辑(computeIfAbsent)是工程上的经典难点,需保证结果集排序、处理 NULL 列,极易出错。
  • 创建订单的明细插入使用了循环单条 update,在数据量大时需改为 batchUpdate 或利用数据库的多行 INSERT 语法,但这都需要手工改写,体现其性能优化的灵活性。

3. Spring Data JDBC:聚合驱动的持久化

3.1 设计哲学:没有 EntityManager 的"聚合仓库"

Spring Data JDBC 是一个极具独特设计哲学的框架,它明确拒绝 了 JPA 的 EntityManager、一级缓存、懒加载、自动脏检查和透明持久化。其核心理念是:持久化的边界就是 DDD 聚合的边界 。这个定位使其介于传统数据映射器和完整 ORM 之间,严格遵循 DDD 聚合仓库模式------实体不包含持久化行为,但通过 CrudRepository.save(aggregateRoot) 触发全量聚合的持久化。

  • 无懒加载 :聚合内部的所有关联(如 Order 包含 List<OrderItem>)在加载时就会一次性全部填充。跨越聚合根的引用(如 Order.getUserId())仅存储外键 ID,不维护对象引用。这从根本上避免了一级缓存带来的数据滞后和 N+1 查询问题。
  • 写入语义清晰 :调用 CrudRepository.save(aggregateRoot) 时,框架会全量更新整个聚合 。即使只修改了订单金额,该聚合内所有嵌套实体也将被重新 DELETEINSERT(或执行全字段 UPDATE,取决于配置)。这种看似"浪费"的行为消除了对象状态与数据库行状态的差异判断,极大简化了并发控制和性能推理。
  • 无持久化上下文 :对象只在 save() 那一刻变为持久化,其余时间就是普通 Java 对象。这避免了"修改一个字段在事务提交时自动发出几十条 UPDATE"的常见 JPA 灾难,让写操作的行为完全受开发者控制。
  • 并发安全 :通过 @Version 字段实现乐观锁,全量 UPDATE 语句会带上 WHERE id = ? AND version = ?,若返回更新行数为 0 则抛出 OptimisticLockingFailureException,行为高度可预测。

3.2 代码示例:聚合的定义与持久化

java 复制代码
// ---------- 聚合根定义 ----------
@Table("orders")                // 映射到 orders 表
public class Order {
    @Id
    private Long id;            // 聚合根标识
    private Long userId;        // 跨聚合引用,仅存储外键 ID,不持有 User 对象
    @Version
    private int version;        // 乐观锁版本号,每次更新自动递增
    private LocalDateTime orderDate;
    private BigDecimal amount;
    @MappedCollection(idColumn = "order_id")  // 子实体集合,order_id 为外键
    private List<OrderItem> items = new ArrayList<>();
    // 省略 getters/setters(或使用 Lombok @Data)
}

@Table("order_items")
public class OrderItem {
    @Id
    private Long id;
    private String productName;
    private int quantity;
    // 注意:OrderItem 中不需要持有 Order 引用,保持单向依赖
}

// ---------- Repository ----------
public interface OrderRepository extends CrudRepository<Order, Long> {
    // 方法名推导,按 userId 查询
    List<Order> findByUserId(Long userId);
}

// 使用 ------ 查询订单列表,聚合内 items 自动填充
@Transactional
public List<Order> getUserOrders(Long userId) {
    // 框架根据 UserId 查询 orders 表,再按 order_id 查询 order_items 表,自动组装
    return orderRepository.findByUserId(userId);
}

// 创建订单 ------ 一条 INSERT orders + 若干 INSERT order_items
@Transactional
public Long createOrder(Order order) {
    order = orderRepository.save(order);  // 全量持久化聚合
    return order.getId();
}

// 更新金额 ------ 即使只改 amount,框架也会更新整个聚合,DELETE 旧的 items 再 INSERT 新的
@Transactional
public void updateOrderAmount(Long orderId, BigDecimal newAmount) {
    Order order = orderRepository.findById(orderId).orElseThrow();
    order.setAmount(newAmount);
    orderRepository.save(order); // 全量更新,包含所有字段和子实体
}

代码解读

  • 样板代码几乎消失,聚合内的映射由框架根据命名约定和注解自动处理,@MappedCollection 标注的集合会被级联插入/更新/删除。
  • 跨聚合引用仅存储 ID(userId),符合 DDD"聚合间通过 ID 引用"的最佳实践,天然适配微服务风格的模块隔离。
  • 全量更新是双刃剑:对于聚合内子实体较少(如订单一般只有几个明细)的场景,简单可预测;但在聚合子实体过多(如数百个订单明细)时,批量全量更新 SQL 量可能急剧膨胀,此时应拆分聚合或回退到 MyBatis/JdbcTemplate。

3.3 并发下的全量更新安全性

由于每次 save 都生成包含 @Version 字段的 WHERE 条件,如果另一个事务已经在当前事务读取后修改了同一行,UPDATE 将影响 0 行,Spring Data 会抛出 OptimisticLockingFailureException,保证原子性。这与 JPA 的乐观锁语义完全一致,但实现更直接------无需对比新旧字段值,只依赖版本号。


4. Spring Data JPA / Hibernate:持久化上下文与自动映射

4.1 EntityManager 与工作单元模式

JPA 规范的核心抽象是 EntityManager,Hibernate 的 Session 是其实现。它实现了 工作单元(Unit of Work)身份映射(Identity Map)

  • 持久化上下文EntityManager 内部维护一个 Map 结构,缓存当前会话中所有已加载或已持久化的实体。同一数据库行在同一个上下文中无论查询多少次,返回的都是同一个 Java 对象(同一身份)。这保证了对象状态的唯一性,防止多个副本互相覆盖。
  • 自动脏检查 :在 EntityManager.flush() 或事务提交时,Hibernate 会比较持久化上下文中实体的当前属性值与加载时保存的原始快照。若发现差异,自动生成对应的 UPDATE 语句,实现了 透明写入 ------ 开发者只需修改 Java 对象,不需要显式调用 save()
  • 级联操作 :通过 cascade = CascadeType.ALL,对聚合根的操作(持久化、更新、删除)可以扩散到内部关联实体,实现面向对象的一次性操作。

4.2 JPA 持久化上下文与自动脏检查序列图

sequenceDiagram participant App as 应用代码 participant EM as EntityManager participant PC as 持久化上下文(一级缓存) participant DB as 数据库 App->>EM: em.find(Order.class, 1L) EM->>PC: 检查是否已缓存 (Map) alt 未缓存 EM->>DB: SELECT * FROM orders WHERE id=1 DB-->>EM: 返回行数据 EM->>PC: 创建实体,保存原始快照 (Object[]) EM-->>App: 返回托管实体对象 else 已缓存 EM-->>App: 返回同一实例 end App->>App: order.setAmount(new BigDecimal("999")) Note over App,PC: 实体内部标记为脏
持久化上下文记录状态变更 App->>EM: 事务提交触发 flush() EM->>PC: 遍历所有托管实体,比对当前属性与原始快照 PC->>PC: 发现 order.amount 改变 PC->>DB: UPDATE orders SET amount=999, version=2
WHERE id=1 AND version=1 DB-->>PC: 更新成功 (影响行数1) EM->>DB: COMMIT

图4 说明(四层)

  • 图表意图 :展示 EntityManager.find()、脏检查触发、自动 UPDATE 的完整时序。
  • 关键元素:持久化上下文的缓存查询、原始快照保存、flush 时的深度对比、自动 UPDATE 生成。
  • 设计思想:通过维护对象状态的两份副本(当前状态与原始快照),实现透明的对象修改即持久化。这是极大提高开发效率的机制,也是批量写性能陷阱的根源------如果一个事务管理了大量实体,脏检查的成本会线性增长。
  • 对应正文:4.1 节原理的可视化,为反模式 8.3(意外批量更新)和 8.4(长事务内存膨胀)提供理论基础。

4.3 代码示例:JPA 实现同样业务

java 复制代码
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version
    private int version;        // 乐观锁

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;          // 跨聚合引用,持有 User 对象但设为懒加载

    private LocalDateTime orderDate;
    private BigDecimal amount;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    // 标准 getter/setter,以及便利方法 addItem/removeItem
}

@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
    private int quantity;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    // getter/setter...
}

public interface JpaOrderRepository extends JpaRepository<Order, Long> {
    // 使用 @EntityGraph 避免 N+1 查询
    @EntityGraph(attributePaths = "items")
    List<Order> findByUserId(Long userId);
}

// 查询订单列表 ------ 极简,但需注意 fetch 策略
@Transactional(readOnly = true)
public List<Order> getUserOrders(Long userId) {
    return jpaOrderRepository.findByUserId(userId);
    // @EntityGraph 确保 items 集合一次性加载,避免懒加载
}

// 创建订单 ------ 级联保存
@Transactional
public Long createOrder(Order order) {
    order = jpaOrderRepository.save(order); // 级联插入 Order 和 Items
    return order.getId();
}

// 更新金额 ------ 透明脏检查,无需显式 save
@Transactional
public void updateOrderAmount(Long orderId, BigDecimal newAmount) {
    Order order = jpaOrderRepository.findById(orderId).orElseThrow();
    order.setAmount(newAmount); // 事务提交时自动执行 UPDATE
}

代码解读

  • 声明式查询方法自动实现 SQL 生成,@EntityGraph 显式指定关联加载策略,是避免 N+1 查询的标准手段。
  • cascade = CascadeType.ALL 保证聚合整体持久化,orphanRemoval = true 确保从集合中移除的子实体会被删除。
  • 更新操作仅需修改对象属性,无需显式 save(),这是透明持久化的典型体现。但过度依赖透明持久化可能导致大量隐式 SQL,需要开发者对持久化上下文状态有深刻认知。

5. MyBatis:显式 SQL 与半自动数据映射器

5.1 MapperProxy 动态代理原理与执行流程

MyBatis 的核心机制是基于 JDK 动态代理的 MapperProxy。当调用 orderMapper.findByUserId(1L) 时,调用被代理拦截,查找对应 XML 或注解中定义的 SQL 语句,通过 SqlSession 执行 JDBC 操作,并将返回结果经过 ResultMap 映射为配置的目标对象。

5.2 MyBatis 动态代理序列图

sequenceDiagram participant Caller as 调用方 participant Proxy as MapperProxy (动态代理) participant Config as Configuration (注册中心) participant Session as SqlSession participant Exec as Executor (执行器,含缓存) participant DB as 数据库 Caller->>Proxy: orderMapper.findByUserId(1L) Proxy->>Config: 获取 MappedStatement (namespace+id) Config-->>Proxy: 返回 SQL 定义、ResultMap、参数映射等完整配置 Proxy->>Session: selectList("com.example.OrderMapper.findByUserId", 1L) Session->>Exec: 委托执行器(处理一级/二级缓存) Exec->>DB: JDBC: PreparedStatement 执行 SQL,传入参数 DB-->>Exec: ResultSet Exec-->>Exec: 应用 ResultMap 规则映射为 List,填充关联集合 Exec-->>Session: List Session-->>Proxy: List Proxy-->>Caller: List

图5 说明(四层)

  • 图表意图:展示 MyBatis 方法调用到 SQL 执行的全链路,突出动态代理和映射自动化。
  • 关键元素MapperProxy 拦截、MappedStatement 查找(缓存)、ResultMap 嵌套对象映射为关键步骤。
  • 设计思想:MyBatis 不做任何对象状态管理,每次调用都是全新的 JDBC 执行,性能路径完全透明,SQL 与业务代码解耦但显式可控。
  • 对应正文:5.3 节的代码实现中,SQL 与映射分离的设计优势在此体现。

5.3 代码示例:MyBatis 实现

java 复制代码
// ---------- Mapper 接口 ----------
@Mapper
public interface OrderMapper {
    List<Order> findByUserId(@Param("userId") Long userId);
    void insertOrder(Order order);              // 可使用 useGeneratedKeys 回填主键
    void insertOrderItems(@Param("orderId") Long orderId, @Param("items") List<OrderItem> items);
}

// ---------- XML 映射 ----------
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">

    <!-- 结果映射定义:复杂对象映射的核心,一次性配置,多处复用 -->
    <resultMap id="orderResultMap" type="Order">
        <id property="id" column="order_id"/>
        <result property="orderDate" column="order_date"/>
        <result property="amount" column="amount"/>
        <!-- collection 映射嵌套集合,ofType 指定集合内元素类型 -->
        <collection property="items" ofType="OrderItem" columnPrefix="item_">
            <id property="id" column="id"/>
            <result property="productName" column="product_name"/>
            <result property="quantity" column="quantity"/>
        </collection>
    </resultMap>

    <select id="findByUserId" resultMap="orderResultMap">
        SELECT o.id   AS order_id,
               o.order_date,
               o.amount,
               oi.id  AS item_id,
               oi.product_name AS item_product_name,
               oi.quantity     AS item_quantity
        FROM orders o
                 LEFT JOIN order_items oi ON o.id = oi.order_id
        WHERE o.user_id = #{userId}
        ORDER BY o.id
    </select>

    <!-- 插入订单,回填主键 -->
    <insert id="insertOrder" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
        INSERT INTO orders (user_id, order_date, amount)
        VALUES (#{userId}, #{orderDate}, #{amount})
    </insert>

    <!-- 批量插入明细,利用 MyBatis 动态 SQL foreach 优化 -->
    <insert id="insertOrderItems">
        INSERT INTO order_items (order_id, product_name, quantity) VALUES
        <foreach collection="items" item="item" separator=",">
            (#{orderId}, #{item.productName}, #{item.quantity})
        </foreach>
    </insert>
</mapper>

// 使用 ------ 查询和写入,所有 SQL 可见
@Transactional
public Long createOrder(Order order) {
    orderMapper.insertOrder(order);      // 主键回填至 order.id
    if (!order.getItems().isEmpty()) {
        orderMapper.insertOrderItems(order.getId(), order.getItems()); // 批量插入明细
    }
    return order.getId();
}

public List<Order> getUserOrders(Long userId) {
    return orderMapper.findByUserId(userId);
}

代码解读

  • SQL 完全显式,结果映射通过 <resultMap> 声明,无需编写逐行的 RowMapper。对开发者而言,复杂关联的映射只定义一次即可复用。
  • 插入明细使用了 <foreach> 实现批量插入,这是 MyBatis 动态 SQL 的典型应用,兼顾了高性能和代码可读性。
  • MyBatis 与 JdbcTemplate 相比,最大进步是自动化结果映射 (特别是嵌套集合处理)和动态 SQL 生成,同时保持了 SQL 的直接控制权。与完整 ORM 相比,MyBatis 无托管状态,每次查询都返回全新的对象,没有脏检查开销。

5B. 四大技术的定位关系总览(新增)

5B.1 从纵向演进看关系:抽象层次的"升维"

上述四种技术并不是四个孤立的选项,而是在解决阻抗不匹配这条主线上逐级叠加抽象的结果。可以用一个类比来理解它们的分工:如果把数据访问比作"驾驶",那么:

  • JDBC 是直接操纵引擎、离合、油门。
  • JdbcTemplate 将油门和刹车标准化,但方向还需要你自己打。
  • MyBatis 提供了 GPS 导航路线(SQL 显式定义)并辅助自动挡(结果映射),但路线仍由你指定。
  • Spring Data JDBC 提供了"辅助驾驶"级别的聚合管理,在一定范围内(聚合内)自动行驶。
  • JPA 则是"完全自动驾驶",你只需告诉它目的地,它负责规划路径(生成 SQL),但偶尔可能走错路(产生意外 SQL)。

从抽象层级看,它们的演进路径是:

  1. JdbcTemplate :抽象了资源管理 (连接、语句、结果集),但保留了SQL 与映射的手动性。
  2. MyBatis :在 JdbcTemplate 的基础上抽象了结果映射动态 SQL,但仍要求显式编写 SQL。
  3. Spring Data JDBC :抽象了聚合持久化边界,通过聚合元数据自动生成单表查询和全量写入,不再需要为每个实体编写 SQL,但也不管理对象状态。
  4. JPA/Hibernate :最后抽象了对象状态(持久化上下文、脏检查),让开发者几乎完全脱离 SQL 思维。

这一演进的核心矛盾是:控制力 vs 自动化。左边的方案必须关心数据库细节,但可以获得最佳性能与可预测性;右边的方案几乎不用写 SQL,但一旦出现性能问题,排查和优化的成本极高。

5B.2 从横向互补看关系:同一项目的多栈协同

它们之间的关系并非零和,而是在同一项目中互为补充。大型工程通常不会只用一种技术,而是根据业务模块的特性混合部署:

  • JPA/Spring Data JDBC 管理核心业务领域模型的持久化,将复杂度收敛在聚合内。
  • MyBatis 负责所有复杂查询、报表、统计等跨聚合、跨表的只读场景,提供强大的 SQL 控制力。
  • JdbcTemplate 作为"逃生舱",当批量处理、特定 SQL 优化或极简 CRUD 时,直接穿透所有抽象,以最低成本完成工作。

它们共享 Spring 的 @TransactionalDataSource,使得三者可以在同一个事务内无缝协同,无需额外的分布式事务协调。

5B.3 在 Spring Data 家族中的归属关系

从 Spring 生态的视角看,这些技术分属不同项目,但都在 Spring 统一的数据访问哲学下协同工作:

  • JdbcTemplate 属于 spring-jdbc 模块,是 Spring 对所有 JDBC 操作的基础封装,也是一切更高级抽象的地基。
  • Spring Data JDBCSpring Data JPA 都归属于 Spring Data 项目,遵循统一的 Repository 编程模型(CrudRepositoryPagingAndSortingRepository),使得开发者可以以几乎完全相同的方式声明数据访问接口,只是底层的映射引擎完全不同。
  • MyBatis 由 MyBatis 社区维护,但通过 mybatis-spring-boot-starter 完美融入 Spring 体系,其 Mapper 接口与 @Autowired 无缝集成,SqlSession 生命周期与 Spring 事务同步,感觉上就像 Spring 原生组件。

这种归属关系意味着:无论你选择哪种技术,都能享受到 Spring 提供的 声明式事务统一异常转换依赖注入 的支持,大大降低了技术切换和混合使用的集成成本。


6. 四大技术多维度深度对比与 Spring 事务统一性

6.1 控制粒度

  • JdbcTemplate:最大,SQL 编写、参数绑定、结果集遍历、对象组装全部手动控制。
  • MyBatis:SQL 手动编写,参数和结果映射通过 XML/注解配置自动完成,动态 SQL 提供灵活的控制手段。
  • Spring Data JDBC :SQL 自动生成(基于方法名或 @Query),但聚合设计直接决定生成的 SQL 效率。除非使用 @Query,否则 SQL 完全由框架生成。
  • JPA:SQL 完全自动生成,开发者基本脱离 SQL 编写,控制力最弱。可通过 JPQL、Criteria API 或 Querydsl 部分介入。

6.2 开发效率

  • JPA :方法名推导(如 findByNameAndDateBetween)和透明持久化带来极高效率,样板代码最少。启动时的元模型扫描可能会增加发现速度,但整体开发速度最快。
  • Spring Data JDBC :基于方法命名约定查询,写入通过 save() 一条龙完成,无需手写映射。无复杂关联时效率高,但跨聚合查询仍需手写 SQL。
  • MyBatis:需手写 SQL 和结果映射,但 MyBatis Generator(MBG)可生成基础 CRUD 和映射,减少重复劳动。复杂查询的开发效率高于 JPA Criteria。
  • JdbcTemplate:效率最低。每条 SQL 都需完整编写映射逻辑,缺少复用机制。仅适合极少数关键路径或简单查询。

6.3 性能可预测性

  • JdbcTemplate/MyBatis:SQL 显式,执行路径 100% 由开发者控制,性能行为完全可预测。生产环境排查慢查询时,可以快速在代码或配置文件中定位 SQL 文本。
  • Spring Data JDBC:全量 UPDATE 可能带来额外的数据库开销,但聚合通常较小(如订单+明细),且写入模式高度确定,没有隐式脏检查。预测性良好,但需注意大聚合的吞吐量。
  • JPA :透明写入、懒加载、级联操作可能在运行时产生大量隐藏的 SQL。开发者如果不时刻监控生成的 SQL(例如通过 spring.jpa.show-sql=true 或日志),性能行为将难以预测。这是 JPA 在生产中最具争议的问题之一,也是反模式 8.3 和 8.4 的根源。

6.4 DDD 聚合契合度

  • Spring Data JDBC:设计上天然契合 DDD 聚合模式------聚合根标识唯一,跨聚合引用仅存 ID(非对象引用),级联全量持久化,无延迟加载打破边界。
  • JPA :通过 @OneToMany(cascade=ALL, orphanRemoval=true) 可以实现聚合,但对象引用关系(如 @ManyToOne 持有其他聚合)容易模糊聚合边界。懒加载和持久化上下文可能让聚合内的对象在事务之外还存在"存活状态",带来不安全的调用。
  • MyBatis/JdbcTemplate:无聚合概念。需要在 Repository 层自行编写代码维护聚合的不变量和持久化逻辑,但同时也因此获得了根据业务需求自由设计持久化流程的弹性。

6.5 复杂查询支持

  • MyBatis:最灵活,直接编写任意复杂 SQL,支持动态拼接、存储过程调用,尤其适合报表和多表关联。DBA 可以直接审查 SQL 文件。
  • JdbcTemplate :同样灵活但需要手动映射。当查询结果不映射到领域对象,而是直接输出 List<Map<String, Object>> 时,代码可以极简。
  • JPA:JPQL/HQL 查询能力有限,无法使用数据库特有函数或复杂子查询。Criteria API 强类型但冗长,Querydsl 流畅但需要额外元模型生成步骤。复杂统计查询通常需要降级为原生 SQL。
  • Spring Data JDBC :不支持复杂 JOIN,@Query 可用手写 SQL 弥补,但不具备 MyBatis 的动态 SQL 能力。对于单聚合内查询,方法名推导足够;对于跨聚合报表,应结合 MyBatis 或 JdbcTemplate

6.6 事务管理统一性

Spring 的 @Transactional 为上述所有技术提供了统一的事务声明式管理模型。其背后适配机制不同,但对于开发者而言,编程模型完全一致:

  • JDBCDataSourceTransactionManagerConnection 绑定到当前线程的 TransactionSynchronizationManagerJdbcTemplate 通过 DataSourceUtils.getConnection() 获取该连接。
  • JPAJpaTransactionManagerEntityManager 绑定到事务同步资源中。同一个 EntityManager(对应 Hibernate Session)贯穿整个事务,保证了持久化上下文的一致性。
  • MyBatis :MyBatis-Spring 通过 SqlSessionTemplate 管理 SqlSession 生命周期,自动将其绑定到 Spring 事务。在同一个 @Transactional 方法中,多次 Mapper 调用使用同一个 SqlSession,可利用一级缓存。

混合使用的事务协同 :同一个 @Transactional 方法中可以混合调用 JPA Repository 和 MyBatis Mapper,只要它们配置使用同一数据源 。此时 JpaTransactionManager(或 DataSourceTransactionManager)管理连接,MyBatis 的 SqlSessionFactory 基于同一 DataSource 创建,二者共享同一个底层 JDBC 连接,提交与回滚同步。

6.7 学习曲线

  • JdbcTemplate:很低,仅需 JDBC 基础和 Spring 注入概念。但熟练掌握其 batch 操作和异常体系仍需实践。
  • MyBatis:中等,需要学习 XML 映射语法、动态 SQL 标签、插件机制以及 Spring 集成配置。
  • Spring Data JDBC:低,概念模型简单,DDD 聚合根是唯一前置知识。适合希望快速上手且排斥 JPA 复杂性的团队。
  • JPA:最高。不仅需要精通映射注解和 JPQL,还必须深入理解持久化生命周期(瞬时、托管、游离、删除)、实体状态、脏检查机制、级联类型、Fetch 策略、查询缓存、二级缓存和 FlushMode 等大量概念。如果不理解这些,很容易误用并造成生产事故。

7. 场景化选型决策框架

7.1 四大技术适用场景总结

  • JdbcTemplate:极简 CRUD、批量处理(ETL)、对特定 SQL 有极致微调的流式查询、不引入额外框架依赖的轻量级模块。
  • Spring Data JDBC:领域模型清晰、聚合根明确的 DDD 微服务;强调简单且可预测的持久化行为;拒绝 JPA 复杂性和懒加载的团队;需要明确的聚合写入语义的场景。
  • JPA/Hibernate:标准企业 CRUD、对象关系相对简单但数量众多、需要快速迭代的原型或内部系统、有明确乐观锁需求、团队 JPA 技能成熟的情境。
  • MyBatis:复杂查询主导型业务、多表关联的报表、遗留数据库(表结构不规范)、DBA 需要审查所有 SQL、对 SQL 有极致控制欲的团队。

7.2 选型决策流程图

flowchart TD Start((开始选型)) --> Q1{查询是否大量且复杂
(多表关联、报表、统计分析)?} Q1 -- 是 --> Q2{领域模型是否有
清晰的聚合根,且
实体关系简单?} Q2 -- 是 --> R1[**Spring Data JDBC**
复杂查询可用 MyBatis 搭配] Q2 -- 否 --> R2[**MyBatis**
直接控制 SQL,灵活映射] Q1 -- 否 --> Q3{团队是否愿意投入
学习并精通 JPA?} Q3 -- 是 --> Q4{模型是否包含
大量继承、多态、
复杂对象引用?} Q4 -- 是 --> R3[**JPA/Hibernate**
发挥 ORM 透明持久化优势] Q4 -- 否 --> Q5{是否需要
完全掌握 SQL
和数据库交互路径?} Q5 -- 是 --> R2 Q5 -- 否 --> R1 Q3 -- 否 --> Q6{是否必须
完全控制 SQL 或
有遗留数据库?} Q6 -- 是 --> R2 Q6 -- 否 --> R4[**JdbcTemplate** 或 Spring Data JDBC
根据样板代码接受程度选择]

图6 说明(四层)

  • 图表意图:以三层分支条件(查询复杂度、领域纯度、团队技能/倾向)决策四种技术的最佳归宿。
  • 关键元素:查询复杂度是最强分支因子;聚合根清晰度决定了是否适合 Spring Data JDBC;团队 JPA 能力和 SQL 控制欲作为次级权重。
  • 设计思想:选型是一个多因素权衡的决策过程,而非简单的技术优劣对比。必须匹配"业务特征×领域模型纯度×团队技能×未来演进"的四维矩阵。
  • 对应正文:7.3 节混合策略可作为"非此即彼"之外的最高阶实践。

7.3 混合策略配置与代码示例

在大型项目中,最佳实践往往是在不同模块采用不同技术,形成因地制宜的多栈数据访问层

  • 核心业务领域(如订单、用户、库存聚合):使用 JPA 或 Spring Data JDBC 保证开发效率与聚合一致性。
  • 复杂查询和报表:使用 MyBatis 管理复杂 SQL 和动态条件,甚至直接查询字库的物化视图。
  • 高性能批量处理 :使用 JdbcTemplatebatchUpdate 或直接 JDBC 以获得最大吞吐量。

application.yml 配置示例(Spring Boot 3.x + PostgreSQL):

yaml 复制代码
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: user
    password: pass
    hikari:
      maximum-pool-size: 20
      connection-timeout: 30000
  jpa:
    hibernate:
      ddl-auto: validate          # 生产环境禁止自动建表
    show-sql: true
    open-in-view: false           # 必须关闭 OSIV
    properties:
      hibernate:
        format_sql: true
        default_batch_fetch_size: 100  # 优化懒加载集合的批量查询
  # MyBatis 配置
mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.domain
  configuration:
    map-underscore-to-camel-case: true
    default-fetch-size: 100

混合使用代码示例

java 复制代码
@Service
public class OrderReportService {

    private final JpaOrderRepository jpaOrderRepo;   // JPA 管理核心业务
    private final OrderMapper orderMapper;           // MyBatis 做复杂报表
    private final JdbcTemplate jdbcTemplate;         // JdbcTemplate 做批量插入

    /**
     * 生成报表:查询核心订单(JPA),获取报表维度明细(MyBatis),
     * 将结果批量写回报表表(JdbcTemplate),三者共处同一事务。
     */
    @Transactional
    public void generateDailyReport(LocalDate date) {
        // 1. JPA 负责标准查询
        List<Order> orders = jpaOrderRepo.findByOrderDate(date);

        // 2. MyBatis 负责复杂聚合查询
        List<ReportRow> reportRows = orderMapper.aggregateReportData(date);

        // 3. JdbcTemplate 批量写入报表结果
        jdbcTemplate.batchUpdate(
            "INSERT INTO daily_report (order_id, amount, dimension) VALUES (?, ?, ?)",
            reportRows,
            100,
            (ps, row) -> {
                ps.setLong(1, row.getOrderId());
                ps.setBigDecimal(2, row.getAmount());
                ps.setString(3, row.getDimension());
            }
        );
    }
}

事务说明 :三种技术共用一数据源,@Transactional 启动事务后,底层连接被绑定。JPA 通过 EntityManager、MyBatis 通过 SqlSession、JdbcTemplate 通过 DataSourceUtils 均获得同一连接,整个方法作为一个原子事务提交或回滚。

7.4 遗留系统迁移路径

从大面积使用 JdbcTemplate 向更高抽象迁移,可采用"绞杀者模式"逐步演进:

  1. 抽取映射层 :先创建 MyBatis Mapper 替代部分复杂查询的 RowMapper 代码,保持 SQL 不变,仅将映射自动化。效果立竿见影,风险可控。
  2. 引入聚合管理 :对业务一致性要求强的聚合(如用户-角色-权限),逐步引入 Spring Data JDBC 的 Repository,与原有 JdbcTemplate 的写入共存,通过双写或逐步切换。
  3. JPA 尝试:对具备标准 CRUD 且无复杂关联的核心模块,可择机引入 JPA。但必须严格关闭 OSIV,开启 SQL 日志监控,并使用单元测试验证无隐式批量更新。

8. 常见 ORM 反模式预警

8.1 Open Session In View (OSIV) 与连接池耗尽

Spring Boot 默认开启 OSIV(spring.jpa.open-in-view=true),导致 EntityManagerSession 在视图渲染完毕后才关闭。数据库连接被绑定在整个 HTTP 请求处理周期,即使业务逻辑已完成。当视图层尝试遍历懒加载属性(如 Thymeleaf 模板访问 order.customer.name)时,会触发额外的 SQL,进一步延长连接占用。在高并发场景下,连接池很快耗尽,阻塞其他请求获取连接,最终导致服务不可用。该问题的症状与 JDBC 系列第 10 篇"连接池泄漏"完全一致。

告警日志示例(HikariCP):

csharp 复制代码
HikariPool-1 - Connection is not available, request timed out after 30000ms.
HikariPool-1 - Pool stats (total=20, active=20, idle=0, waiting=15)

改进措施:在配置中显式关闭 OSIV:

yaml 复制代码
spring:
  jpa:
    open-in-view: false

关闭后,视图层(或 Controller 层之外)调用懒加载属性将抛出 LazyInitializationException。解决方案是将数据获取"左移"到 Service 层,使用 JOIN FETCH@EntityGraph 或显式的 DTO 投影一次性加载所需数据,彻底消除跨层泄露数据库连接的风险。此案例可直接引用第 10 篇连接泄漏导致雪崩的排查方法。

8.2 N+1 查询问题与修复

JPA 默认集合关联为懒加载 (FetchType.LAZY),当遍历查询结果并访问关联集合时,引发典型的 N+1 问题:

java 复制代码
List<Order> orders = orderRepo.findAll();   // 1 条 SQL 查询所有订单
for (Order o : orders) {
    // 每次访问 items 都触发一条新的 SQL
    System.out.println(o.getItems().size()); // N 条 SQL
}

修复手段(按优先级)

  1. @EntityGraph :在 Repository 方法上声明 @EntityGraph(attributePaths = "items"),强制对该查询使用 EAGER,底层会用 LEFT JOIN FETCH 或批量二次查询(通过 default_batch_fetch_size 优化)一次性加载。
  2. JOIN FETCH JPQL :自定义 @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.userId = :uid")
  3. 批量大小优化 :配置 hibernate.default_batch_fetch_size=100,将 N 条 SQL 优化为 ceil(N/100) 条,显著减少查询次数。

8.3 JPA 意外批量更新

自动脏检查在循环中修改托管实体,事务提交时产生海量惊喜 UPDATE:

java 复制代码
@Transactional
public void applyDiscountBulk(List<Long> orderIds, BigDecimal discount) {
    List<Order> orders = orderRepo.findAllById(orderIds);
    orders.forEach(o -> {
        o.setAmount(o.getAmount().multiply(discount));
        // 无意中修改所有订单,每个订单在 flush 时产生一条 UPDATE
    });
    // 日志输出:
    // Hibernate: update orders set amount=?, version=? where id=? and version=?
    // ... 重复 10,000 次
}

根本原因 :读取的实体处于托管状态,任何属性变更都会被持久化上下文记录,并在 flush 时生成 UPDATE 语句。如果 orderIds 有 10,000 条,会产生 10,000+ 条 SQL。

解决策略

  • 使用 JPQL 批量更新@Modifying @Query("UPDATE Order o SET o.amount = o.amount * :discount WHERE o.id IN :ids"),直接执行数据库 SQL,跳过托管状态。
  • 限制实体读取 :若非必须,不加载实体。对于纯属性更新,使用 StatelessSession(绕过持久化上下文)或原生 JDBC。
  • 分批次处理并定时 flush()/clear() :若确实需要实体更新,每处理完 50 条调用一次 em.flush()em.clear(),释放持久化上下文内存并减少单次脏检查的遍历量。

8.4 长事务中一级缓存膨胀导致 OOM

持久化上下文的生命周期与事务一致。如果在 @Transactional 方法内依次加载大量实体,并执行外部 I/O(如 RPC 调用、消息队列发送),一级缓存将持续持有所有已加载实体的引用及其原始快照副本,无法被 GC 回收,最终引发 OutOfMemoryError。此问题与第 10 篇堆外内存泄漏导致 OOM 排查 处于同一类内存管理思维,可通过监控 heap dump 中的 EntityEntry 数量发现。

避免方案

  • 缩短事务作用域 :只在必要的写入操作上标记 @Transactional,将只读操作分离为无事务或 readOnly=true 的方法。
  • 定期 clear() 持久化上下文em.clear() 可以立即将一级缓存中所有实体变为游离状态,释放内存。在批量处理循环中应严格执行。
  • 使用 DTO 投射 :查询时直接返回 new com.example.OrderDto(...) 指定字段,不加载完整实体。

8.5 MyBatis 动态 SQL 维护性陷阱

<if><choose><foreach> 嵌套超过三层,业务逻辑和 SQL 拼接混杂,导致 XML 可读性急剧下降,且生成的 SQL 难以静态验证:

xml 复制代码
<select id="search" resultMap="orderMap">
    SELECT * FROM orders WHERE 1=1
    <if test="userId != null"> AND user_id = #{userId} </if>
    <if test="startDate != null">
        <if test="endDate != null"> AND order_date BETWEEN #{startDate} AND #{endDate} </if>
        <if test="endDate == null"> AND order_date >= #{startDate} </if>
    </if>
    <!-- ... 更多条件 -->
</select>

改进方向

  • 对极端复杂的动态查询,使用 MyBatis @SelectProvider@Lang 结合自定义 SQL Builder,将 SQL 构造逻辑转移到 Java 代码中,回归类型安全和调试能力。
  • 考虑引入 JOOQ 作为查询端专用工具,它完美平衡了类型安全和 SQL 直接控制力。
  • 定义 SQL 视图或将查询拆分为多个具体方法,减少动态分支。

9. 面试高频专题

1. JDBC、JdbcTemplate、MyBatis、Spring Data JDBC、JPA 之间的层次关系与演变逻辑?

  • 一句话回答 :自底向上依次为:纯 JDBC → JdbcTemplate(自动化资源管理)→ MyBatis(自动化结果映射)→ Spring Data JDBC(聚合边界持久化)→ JPA/Hibernate(透明持久化上下文),抽象层级逐步升高,控制力逐步减弱,开发效率逐步提高。
  • 详细解释 :JDBC 提供最基础的连接和语句执行,但资源管理繁琐且所有映射手工。JdbcTemplate 用模板方法封装了连接获取、语句创建、结果集遍历与异常转换,使资源管理自动化,但映射仍由 RowMapper 手工完成。MyBatis 在此基础上抽象了结果映射和动态 SQL,开发者通过 XML 或注解定义映射规则和 SQL,消除了大量逐列 getString 代码。Spring Data JDBC 进一步抽象了聚合持久化,根据聚合根和元数据自动生成 CRUD SQL,并采用全量写入策略,但无持久化上下文。JPA/Hibernate 则在最顶层,通过 EntityManager 管理对象状态,提供透明脏检查、级联操作等完整 ORM 能力,开发者只需操作对象图。
  • 追问与深度解析
    1. 为什么在 MyBatis 之上还需要 Spring Data JDBC? ------ MyBatis 仍要求为每个查询手写 SQL,而 Spring Data JDBC 对单聚合的 CRUD 实现了零 SQL,且显式强化了 DDD 聚合边界,符合微服务中聚合持久化的模式。
    2. JPA 是否可以替代所有底层方案? ------ 理论上可以,但实际上 JPA 在复杂查询、批量更新、遗留数据库等场景存在不足,因而诞生了混合使用 JPA+MyBatis 的实践。
    3. 这几种技术作为抽象层次能否互换? ------ 不可以随意替换。从底层切换到高层意味着放弃 SQL 控制权,需要评估团队技能和业务特征。反之,从 JPA 降级到 MyBatis 意味着接管 SQL 生成,工作量增加。
    4. 如果用一句话描述它们的本质区别? ------ JdbcTemplate 是帮你管理连接的 SQL 执行器,MyBatis 是帮你管理 SQL 和映射的半自动数据映射器,Spring Data JDBC 是面向聚合的持久化仓库,JPA 是完整的对象级持久化引擎。
  • 加分回答:Spring 体系内,所有这些技术共享同一事务管理器和数据源,因此可以在同一个项目中根据需要选择不同抽象层,形成灵活的数据库访问多栈策略。

2. Spring Data JDBC 为什么取消了 JPA 的一级缓存,这在微服务中有何好处?

  • 一句话回答:取消一级缓存是为了消除透明脏检查的副作用和内存风险,并明确聚合持久化边界,使服务间状态更清晰,天然适合微服务的短事务和无状态模型。
  • 详细解释 :JPA 一级缓存使得实体在持久化上下文中保持同一对象引用,并基于快照自动生成 UPDATE。这种隐式行为在分布式微服务中会导致:①服务在长事务中持有陈旧数据;②对聚合的局部修改可能在提交时意外扩散出大量的 UPDATE;③一级缓存在内存中膨胀无法回收。Spring Data JDBC 的聚合在每次 find 后都是全新对象,没有"托管"生命周期,写操作必须显式 save,且全量更新行为极可预测,这与微服务要求的"明确的资源状态转换"高度一致。
  • 追问与深度解析
    1. 取消缓存会不会导致性能下降? ------ 微服务短事务下,重复加载同一聚合的开销远小于 JPA 脏检查和内存管理开销。热点数据应由分布式缓存(如 Redis)负责,而非数据库层。
    2. 如果聚合很大,还会有性能问题吗? ------ 大聚合本身在 DDD 中视为设计异味,应拆分为更小的聚合。Spring Data JDBC 引用仅存 ID,推动这种拆分,符合微服务拆分原则。
    3. Spring Data JDBC 的乐观锁与 JPA 有何异同? ------ 同样使用 @Version,但 Spring Data JDBC 锁定的是全量 UPDATE 的版本号,JPA 可只更新改动字段。
    4. 无懒加载如何解决跨聚合查询? ------ 通过应用层组合,先加载当前聚合,再通过 ID 调用其他服务或 Repository 获取关联数据,形成最终的 DTO。
  • 加分回答:Spring Data JDBC 刚好位于 CQRS 的"命令侧":它加载完整聚合并整量写入,查询侧则通常由 MyBatis 或执行在只读视图上的 SQL 实现,读写职责清晰分离。

3. MyBatis 如何通过动态代理映射方法到 SQL,它与 JdbcTemplate 和 JPA 的最大区别在哪?

  • 一句话回答 :MyBatis 通过 MapperProxy 拦截接口方法,查找并执行 XML/注解中的 SQL,自动完成结果映射;最大的区别是:相比 JdbcTemplate 它提供了自动结果映射和动态 SQL;相比 JPA 它完全没有对象状态管理和自动 SQL 生成。
  • 详细解释 :当调用 orderMapper.findById(1L) 时,MapperProxyConfiguration 获取对应 MappedStatement(包含 SQL、结果映射、参数映射),然后委托 SqlSession 执行 JDBC 操作。返回的 ResultSetResultMap 自动转换为嵌套对象,开发者无需编写 RowMapper。与 JdbcTemplate 的区别在于减少了 80% 的样板代码;与 JPA 的区别在于,MyBatis 还处于"需要知道 SQL 怎么运行"的层级,没有脏检查、没有透明写入、没有对象生命周期,性能可控性极高。
  • 追问与深度解析
    1. MyBatis 为什么不称为 ORM? ------ ORM 定义包含"对象-关系映射"以及对象状态管理,MyBatis 只做"结果映射",没有"对象状态",属于 Data Mapper 模式。
    2. MyBatis 一级缓存和 Hibernate 一级缓存有何不同? ------ MyBatis 一级缓存是 SqlSession 级别的查询结果缓存,不会维护对象快照;Hibernate 一级缓存是持久化上下文,缓存的是实体状态及原始快照。
    3. MyBatis 的动态 SQL 如何避免 SQL 注入? ------ 通过 #{} 预编译参数,而非 ${} 直接拼接。
    4. 如果 SQL 特别复杂,MyBatis 还能胜任吗? ------ 核心优势恰恰在此,它支持存储过程、复杂嵌套查询、动态条件,可以写任意复杂的 SQL。
  • 加分回答 :MyBatis 的插件机制(Interceptor)利用了对 ExecutorParameterHandlerResultSetHandlerStatementHandler 的代理,可实现分页、加密、审计等无侵入扩展。

4. JPA 的自动脏检查机制原理及其潜在性能陷阱?

  • 一句话回答 :原理是 EntityManager 在 flush 时比对实体当前状态与加载时保存的快照,自动生成 UPDATE;陷阱在于循环修改大量托管实体时产生预料之外的海量 UPDATE。
  • 详细解释 :Hibernate EntityEntry 保存实体的原始数据(Object[])。当事务提交触发 flush() 时,会遍历一级缓存中所有托管实体,用反射或字节码增强获取当前属性值并与快照对账,不一致的就产生 UPDATE。如果在一个 @Transactional 方法内加载了 1 万条订单并逐一微调,就会生成 1 万条 UPDATE,即使你根本没有显式调用 save()。这会导致数据库负载徒增、事务时间拉长、锁竞争加剧。
  • 追问与深度解析
    1. 如何避免脏检查引发的批量更新? ------ 使用 @Transactional(readOnly=true) 可关闭脏检查;或者使用 JPQL 的 UPDATE ... SET ... WHERE;或者直接使用 StatelessSession
    2. 脏检查是否可以优化? ------ Hibernate 支持字节码增强(Bytecode Enhancement)以记录脏属性而非全对象比对,可减少更新字段数,但无法彻底避免。
    3. MyBatis 和 Spring Data JDBC 为什么没有这个问题? ------ MyBatis 无托管状态,必须显式调用 update;Spring Data JDBC 对象未被框架跟踪,只有显式 save() 才执行全量更新。
    4. 能否仅关闭特定方法的脏检查? ------ 可以使用 @Transactional(readOnly = true)Session.setReadOnly(),但这会阻止写操作。
  • 加分回答 :可以通过配置 hibernate.jdbc.batch_size 将多条 UPDATE 合并为一次批量提交,降低网络开销,但不会减少 SQL 数量。

5. 如何决定在项目中使用哪种数据访问技术?

  • 一句话回答:依据查询复杂度、领域模型纯度、团队技能综合决策,可参考决策流程图(图6)进行分支判定,且不应绑定唯一方案。
  • 详细解释:如果查询复杂且多为报表型,应优先 MyBatis 或 JdbcTemplate。如果领域模型具有清晰的聚合根,且无大量继承和复杂对象图,Spring Data JDBC 是平衡开发效率与可控性的上选。如果业务是标准 CRUD,且团队具备 JPA 扎实功底,JPA 能极快交付。最重要的原则是:不要一味追求统一,混合使用往往是最佳实践。
  • 追问与深度解析
    1. 小团队如何选? ------ 若核心成员擅长 SQL,用 MyBatis 可快速落地;若全栈为主,Spring Data JPA 可加速开发。
    2. 为何大型项目推荐混合策略? ------ 因为一个项目内包含多种业务形态:核心领域需要一致性(JPA/Spring Data JDBC),后台报表需要复杂查询(MyBatis),数据同步需要批量吞吐(JdbcTemplate)。
    3. DDD 项目为何偏爱 Spring Data JDBC? ------ 聚合边界的明确性与无持久化上下文的透明性高度吻合 Evans 的聚合设计原则。
    4. 是否所有项目都可以上 JPA? ------ 否。遗留数据库存在大量不规范设计,JPA 生成的 SQL 往往不符合预期且难以优化。
  • 加分回答:采用 CQRS 策略:命令侧使用 Spring Data JDBC,查询侧使用 MyBatis,两个模型物理上可用不同的数据源(读写分离),实现更强的隔离。

6. OSIV 为什么是反模式?如何优雅关闭,关闭后如何解决 View 层懒加载问题?

  • 一句话回答 :OSIV 让数据库连接贯穿整个 HTTP 请求(包括视图渲染),极易耗尽连接池;通过 spring.jpa.open-in-view=false 关闭;视图层所需数据通过 Service 层 DTO 或 JOIN FETCH 提前加载。
  • 详细解释 :OSIV(Open Session In View)是 Spring Boot 默认开启的,本质是让 JPA Session 在 @Transactional 方法结束后仍保持开启,直至视图渲染完成。这允许视图中访问懒加载属性,但导致数据库连接无法释放。当并发量增大或视图存在慢操作(如调用远程服务),连接池耗尽。关闭 OSIV 后,懒加载访问会抛出 LazyInitializationException,最佳实践是让 Service 层返回定制的 DTO(含有视图所需全部数据),或使用 @EntityGraphJOIN FETCH 将必要关联在查询时加载。
  • 追问与深度解析
    1. 关闭 OSIV 后,现有依赖懒加载的模板如何处理? ------ 需重构,在 Controller 进入前就把懒加载属性通过 Hibernate.initialize() 或 DTO 组装完毕。
    2. 是否可以在部分接口开启 OSIV? ------ 技术上可以但强烈不建议,一致关闭可避免隐蔽的并发瓶颈。
    3. 连接池耗尽的监控? ------ 通过 HikariCP 的 metrics 监控 activeConnectionspendingConnections,若关闭 OSIV 后等待线程数显著下降即为验证。
    4. 除了连接池,OSIV 还有什么副作用? ------ 可能导致因意外懒加载而触发额外 SQL,造成不可预测的性能抖动。
  • 加分回答 :在 REST API 中,JSON 序列化同样会触发懒加载并导致 LazyInitializationException 或性能问题,推荐使用 @JsonIgnoreProperties 或使 Hibernate5Module 强制处理懒加载代理为 null,或返回 DTO 投影。

7. 如何在同一个 Spring Boot 项目中同时使用 JPA 和 MyBatis,并保证事务一致?

  • 一句话回答 :共享同一个 DataSource,通过 Spring 的 JpaTransactionManager(或 DataSourceTransactionManager)管理事务边界,MyBatis 的 SqlSession 自动绑定到事务,确保同步提交或回滚。
  • 详细解释 :Spring Boot 会自动配置 HikariCP 数据源,JPA 和 MyBatis 的 starter 都依赖这个数据源。当 @Transactional 开启时,JpaTransactionManager 会从数据源获取连接并绑定到当前线程;MyBatis 的 SqlSessionTemplate 在每次 Mapper 调用时会从 Spring 事务同步管理器中获取已绑定的连接,确保它们使用同一个数据库连接。这样,任何未捕获异常都会导致 JPA 和 MyBatis 的操作一起回滚。
  • 追问与深度解析
    1. 如果同时存在多个事务管理器怎么办? ------ 需用 @Primary 指定一个主事务管理器,或在 @Transactional 注解上显式指定 transactionManager 属性。
    2. 能使用不同数据源吗? ------ 可以,但会引入 JTA 分布式事务协调,复杂度指数级上升,一般不推荐同一 JVM 内为了不同 ORM 搞多数据源。
    3. MyBatis 一级缓存在此行为怎样? ------ 在同一事务中,多次调用同一 Mapper 方法会命中一级缓存,但注意该缓存不感知 JPA 对数据库的修改。
    4. 事务传播级别如何协同? ------ 同样适用 Spring 的标准传播行为,不会因为混合而改变语义。
  • 加分回答 :可以利用 JPA 的 @DomainEvents 发送领域事件,然后在同一事务中由 MyBatis 写入事件表以实现最终一致性,但要确保不跨框架共享正在被 JPA 管理的对象。

8. 复杂查询场景下,JPA Criteria API、Querydsl 和 MyBatis 的取舍?

  • 一句话回答:Criteria API 类型安全但冗长,Querydsl 简洁流畅但需生成元模型,MyBatis 直接写 SQL 最灵活;JOOQ 是第四种强力选项,提供编译安全且完美的 SQL 控制。
  • 详细解释:Criteria API 是 JPA 官方标准,可以通过强类型操作元模型生成查询,但代码可读性和维护性不及 Querydsl。Querydsl 提供流畅 DSL,前后端通用,但需要额外编译步骤生成 Q 类。MyBatis 完全脱离对象模型,直接接触 SQL,最直观,适合需 DBA 审查的场景。JOOQ 则更接近 SQL 的编程化,能生成所有 DDL/DML 的类型安全 Java 代码,非常强大。
  • 追问与深度解析
    1. 性能上有没有差异? ------ 生成的 SQL 可能不同,实际执行计划需用 EXPLAIN 对比。
    2. 是否有动态查询需求? ------ MyBatis 动态 SQL 和 Querydsl BooleanBuilder 都很适合,Criteria API 也支持动态构建 Predicate。
    3. 学习门槛? ------ Criteria API 低->中,Querydsl 中,MyBatis 低->中,JOOQ 中->高。
    4. Spring Data JPA Specification 呢? ------ 轻量级动态查询方案,比 Criteria 更简洁,但表达能力有限。
  • 加分回答:在大型项目中,常将 JPA 用于 CRUD,MyBatis 或 JOOQ 专门负责复杂查询,形成"命令用 JPA,查询用 SQL"的 CQRS 实践。

9. MyBatis 的一级/二级缓存与 Hibernate 缓存体系的差异?

  • 一句话回答 :MyBatis 一级缓存是 SqlSession 级别的查询结果缓存,二级缓存是可跨会话的映射语句级缓存;Hibernate 一级缓存是持久化上下文(维护对象状态和快照),二级缓存需第三方实现,用于实体/集合/查询缓存。
  • 详细解释 :MyBatis 一级缓存默认开启,同一 SqlSession 内相同查询参数返回同一结果对象,但对象之间没有状态关联(没有快照、脏检查)。Hibernate 一级缓存是持久化上下文的核心,记录实体状态,用于保证同一行只对应一个对象,并支撑脏检查。MyBatis 二级缓存基于 Mapper 的命名空间缓存结果数据,不同 SqlSession 可共享,但数据是序列化存储的,需要处理缓存失效。Hibernate 二级缓存需要集成 Ehcache/Hazelcast 等,事务提交时写入,保证与数据库的一致性。
  • 追问与深度解析
    1. 在 Spring 事务中 MyBatis 一级缓存的典型陷阱? ------ 如果同事务中,其他代码(如 JPA)修改了数据库但 MyBatis 缓存不失效,会读到过期数据。
    2. 何时启用 MyBatis 二级缓存? ------ 适用于极少修改的配置表或字典表,且设置合理的 TTL/失效策略。
    3. Hibernate 一级缓存的 OOM 风险? ------ 长事务中大量加载实体,一级缓存不断膨胀,应定时 clear() 或使用 DTO 投射。
    4. MyBatis 二级缓存的序列化开销? ------ 所有缓存对象必须实现 Serializable,在读写时存在序列化/反序列化成本。
  • 加分回答:在 MyBatis 中集成 Redis 作为二级缓存很常见,但需自行保证缓存一致性;Hibernate 的二级缓存对实体更新后的同步有更完善的内建机制。

10. JPA 意外批量更新是如何发生的?怎样避免?

  • 一句话回答 :在托管状态下修改实体属性,事务提交时脏检查自动生成每一条 UPDATE;避免方法是使用 JPQL @Modifying 批量更新,或使用 StatelessSession
  • 详细解释 :见 8.3 节。当实体从 findquery 返回后,处于托管(Managed)状态,任何属性变更都会被持久化上下文记录。事务提交时的 flush() 会遍历所有托管实体,对每一个发生变化的实体生成 UPDATE。如果循环处理大量实体,就会产生等量 UPDATE。解决方法:(1)改用 @Modifying + JPQL UPDATE;(2)分批 flush()/clear() 释放实体;(3)完全不用实体,用 JdbcTemplateStatelessSession 进行批量 DML。
  • 追问与深度解析
    1. 如何监控和发现意外更新? ------ 打开 org.hibernate.SQL 的 DEBUG 日志,观察 SQL 数量是否异常;或使用测试中 @DataJpaTest 结合 SQL 断言。
    2. @Transactional(readOnly=true) 能避免吗? ------ 会跳过脏检查,但也无法执行任何写操作。
    3. Spring Data JDBC 有类似问题吗? ------ 没有,因为它没有托管状态,必须调用 save() 才能写入。
    4. 字节码增强能否只更新变化的字段而不是所有字段? ------ 是的,可以减少 UPDATE 的语句尺寸,但不会减少 UPDATE 的总数。
  • 加分回答:在 CI 流水线中集成性能测试,设定"单次业务操作的 SQL 上限",以此回归检测 ORM 产生的意外 SQL 雪崩。

11. Spring Data JDBC 的全量 UPDATE 在并发下是否安全?如何利用乐观锁?

  • 一句话回答 :安全,通过 @Version 字段在全量 UPDATE 的 WHERE 子句中携带版本号,利用数据库行级锁保证并发写操作的原子性,冲突时抛出 OptimisticLockingFailureException
  • 详细解释 :Spring Data JDBC 的 save 生成的 UPDATE 长这样:UPDATE orders SET ..., version = version + 1 WHERE id = ? AND version = ?。如果两个事务并发读取版本 N,第一个事务更新成功,版本变为 N+1;第二个事务的 WHERE version = N 找不到行,影响行数为 0,框架抛出异常,业务代码可捕获并重试。这与 JPA 的乐观锁语义完全一致,只是更新粒度是全量字段。
  • 追问与深度解析
    1. 如果聚合非常大,全量更新会有什么代价? ------ 会增加网络传输和数据库写入量,尤其是包含数百子实体的聚合,应考虑拆分。
    2. 是否可以不使用 @Version ------ 可以,但会退化到"最后写覆盖",可能导致并发修改丢失。
    3. 全量更新和部分字段更新,在乐观锁的安全性上有差别吗? ------ 无差别,锁定机制都是基于版本号。
    4. 重试机制如何设计? ------ 可使用 Spring Retry 库,在捕获到 OptimisticLockingFailureException 后重新加载最新聚合并重新应用业务变更。
  • 加分回答:在 RESTful 服务中,全量更新与乐观锁恰好匹配 HTTP PUT 语义:客户端提供资源的完整表示,服务端通过版本号防止并发覆盖。

12. 系统设计题:设计一个同时面向普通 CRUD 和管理端复杂报表的应用的数据访问层,要求支持 DDD 聚合和灵活查询,并给出 OSIV 和连接池的协调配置。

  • 简要设计
    1. 领域命令侧 :采用 Spring Data JDBC 管理核心业务聚合(如 Order 聚合根)。聚合内保证业务不变量,写入通过 save() 全量更新,利用 @Version 乐观锁。跨聚合引用仅用 ID,保持微服务边界清晰。
    2. 查询侧(报表) :使用 MyBatis 编写所有复杂报表 SQL,直接查询表或物化视图。定义专用 DTO(如 OrderReportDto),绝不复用领域实体。对极大数据量的报表,使用 MyBatis 的 fetchSize 流式查询。
    3. OSIV 关闭 :在 application.yml 中设置 spring.jpa.open-in-view=false(即使是 Spring Data JDBC + MyBatis,若未来引入 JPA,也应默认关闭)。MyBatis 的 SqlSession 在事务结束后由 Spring 自动关闭,不会跨层泄漏。
    4. 连接池配置 :HikariCP,最大连接数 30(根据压测结果微调),connection-timeout=3000leak-detection-threshold=2000 用于连接泄漏监控,idle-timeout=600000。通过 Prometheus 采集 HikariCP metrics 并配置告警。
    5. 事务管理DataSourceTransactionManager,负责 Spring Data JDBC 和 MyBatis 的事务同步。所有写入操作包裹在短事务中,报表查询使用 @Transactional(readOnly=true) 以享受只读优化及事务上下文。
  • 追问与深度解析
    1. 为什么命令侧用 Spring Data JDBC 而非 JPA? ------ 追求聚合边界的明确性和写入语义高度可预期,避免脏检查。
    2. 报表数据量极大怎么优化? ------ MyBatis 配合 fetchSize 实现流式查询,或定时任务将数据汇聚到 ElastichSearch,数据访问层从 ES 读取。
    3. 若需要跨库操作怎么办? ------ 如果命令和查询分离到不同数据库(CQRS),应通过消息事件(如 Kafka)同步数据,查询侧保持最终一致性。
    4. 如何保证报表不影响业务写入? ------ 可以配置只读数据源(读库/只读副本),MyBatis 的 SqlSessionFactory 使用只读数据源,避免相互争抢连接。
  • 加分回答:采用"数据库视图"作为 MyBatis 直接映射的载体,屏蔽底层表的复杂性和变更,同时可以实施列级权限;连接池使用不同实例隔离事务型和报表型流量。

数据访问技术选型速查表

技术 核心原理 适用场景 禁忌 事务适配
JdbcTemplate 模板方法封装 JDBC,手动映射 批量处理、极致性能、简单查询 复杂对象图映射、大量样板代码 DataSourceTransactionManager
Spring Data JDBC 聚合根持久化,无缓存,全量更新 DDD 聚合、微服务、简单持久化 复杂关联查询、大聚合高频更新 DataSourceTransactionManager
JPA/Hibernate 工作单元,持久化上下文,自动脏检查 标准企业 CRUD,关系简单 复杂查询、遗留库、大流量写 JpaTransactionManager
MyBatis 显式 SQL + 结果映射器,动态代理 复杂 SQL、报表、遗留数据库优化 过度动态 SQL 导致维护难 与 Spring 集成,共用事务管理器

延伸阅读

  • Patterns of Enterprise Application Architecture by Martin Fowler
  • Spring Data in Action by Mark Pollack et al.
  • Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans
  • MyBatis 3 官方文档mybatis.org/mybatis-3/
  • Hibernate ORM 6.x 用户指南hibernate.org/orm/documen...

JDBC 系列的底层深耕在此暂时划下句点,但这条道路将无缝延伸至 MyBatis 深度内核和 Spring Data JPA 的高级持久化策略。理解了对象的阻抗不匹配和四种武器的设计哲学,就能在任何业务场景中做出坚实、安全的架构决策。

相关推荐
Henray20241 小时前
最低公共祖先 LCA
java·面试
shehuiyuelaiyuehao1 小时前
关于进程和线程的关系
java·开发语言
河阿里2 小时前
SpringBoot:项目启动速度深度优化
java·spring boot·后端
AaronCos2 小时前
弄懂java泛型中的extends和super
java·开发语言
用户239526180102 小时前
别再 new 满天飞了!一文搞懂工厂模式,程序员终于不用手搓对象了 😆
java
阿丰资源2 小时前
基于SpringBoot的企业客户管理系统(附源码)
java·spring boot·后端
两年半的个人练习生^_^2 小时前
SpringBoot 项目使用 Jasypt 实现配置文件敏感信息加密
java·spring boot·后端
JAVA学习通2 小时前
开云集致 Java开发 实习 一面
java·开发语言
阿旭超级学得完2 小时前
C++11(初始化)
java·开发语言·数据结构·c++·算法