05. ORM 框架:JPA/Hibernate 与 MyBatis

对比 JPA/Hibernate 与 MyBatis 的设计哲学、性能差异与适用场景,掌握二级缓存、N+1 问题、批量优化等实战技巧

Java ORM 生态圈的核心矛盾是开发效率执行效率的权衡。JPA/Hibernate 通过抽象隐藏 SQL,加速 CRUD 开发;MyBatis 让开发者精确控制每一行 SQL,换取极致性能。本文对比两者设计哲学,聚焦 N+1、缓存策略、批量操作等关键实战问题。

1. ORM 架构对比

JPA / Hibernate
  ┌─────────────────────────────────────┐
  │ EntityManager / Session             │
  │  ├─ 持久化上下文 (Persistence Context)
  │  ├─ 脏检查 (Dirty Checking)
  │  ├─ 延迟加载 (Lazy Loading)
  │  └─ 一级缓存 (Entity Cache)
  └─────────────────────────────────────┘
           ↓ HQL / JPQL / Criteria API
        SQL 生成 (AST → SQL 方言)
           ↓
        JDBC 执行

MyBatis
  ┌─────────────────────────────────────┐
  │ SqlSession                          │
  │  ├─ Mapper 接口代理                 │
  │  └─ 参数映射 / 结果集映射            │
  └─────────────────────────────────────┘
           ↓ 手写 SQL
        XML / 注解定义的 SQL
           ↓
        JDBC 执行

1.1 设计哲学差异

维度JPA/HibernateMyBatis
抽象层级完全抽象(面向对象)半抽象(SQL + 映射)
SQL 可见性自动生成,可通过日志查看完全显式控制
学习曲线陡峭(需理解状态管理)平缓(SQL 即核心)
复杂查询JPQL/Criteria 较笨重XML/注解灵活
数据库无关性强(方言层)弱(需手写多库 SQL)
缓存体系完善的一级/二级/查询缓存需自行集成(默认一级缓存会话级)

2. Hibernate 核心机制

2.1 实体状态模型

// 瞬时态 (Transient) — 未与 Session 关联
Book book = new Book("Java", 100);

// 持久态 (Persistent) — Session 管理
session.persist(book);        // 变为持久态,插入数据库
book.setPrice(200);           // 脏检查 → 自动 UPDATE

// 游离态 (Detached) — 曾持久化但 Session 关闭
session.detach(book);         // / session.close()
book.setPrice(300);           // 无自动操作

// 删除态 (Removed)
session.remove(book);         // 标记删除,flush 时 DELETE

2.2 脏检查与自动刷新

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

Book book = session.find(Book.class, 1L);  // 加载到持久化上下文
book.setPrice(book.getPrice() * 1.1);       // 修改

// tx.commit() 时自动执行:
// 1. flush — 脏检查发现 priceChanged → 生成 UPDATE SQL
// 2. commit — 提交事务
// 无需手动 save()

disable auto-flush(调试 SQL 时)

session.setHibernateFlushMode(FlushMode.MANUAL);

2.3 级联与抓取策略

@Entity
public class Author {
    @OneToMany(mappedBy = "author", 
               cascade = CascadeType.ALL,      // 级联操作
               fetch = FetchType.LAZY,         // 默认延迟加载
               orphanRemoval = true)           // 孤儿删除
    private List<Book> books = new ArrayList<>();
}

抓取策略对比

策略使用场景风险
FetchType.LAZY关联数据不常用N+1 问题(需 JOIN FETCH 或 @BatchSize 批量加载)
FetchType.EAGER关联数据几乎必用过度抓取、Cartesian 积膨胀

2.4 N+1 问题与解决方案

问题:查询 N 个作者 → 每个作者触发 1 次书籍查询 = N+1 条 SQL。

// N+1 触发场景
List<Author> authors = em.createQuery("FROM Author", Author.class).getResultList();
for (Author a : authors) {
    a.getBooks().size();  // 每条触发 SELECT books WHERE author_id=?
}

// 方案 1:JOIN FETCH
FROM Author a JOIN FETCH a.books  // 单条 JOIN SQL

// 方案 2:@BatchSize
@BatchSize(size = 50)  // 每 50 个 Author 批量加载 Books,变 N/50+1 条

// 方案 3:Entity Graph
EntityGraph<Author> graph = em.createEntityGraph(Author.class);
graph.addSubgraph("books");
Map<String, Object> hints = Map.of("javax.persistence.fetchgraph", graph);
Author a = em.find(Author.class, 1L, hints);

// 方案 4:二级缓存 + @Cache
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "books")

2.5 二级缓存配置

<!-- ehcache.xml -->
<cache alias="org.example.Book">
    <expiry>
        <ttl unit="minutes">10</ttl>
    </expiry>
    <heap unit="entries">1000</heap>
</cache>
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Book { }

缓存并发策略

策略适用说明
READ_ONLY不变数据只读,效率高
NONSTRICT_READ_WRITE偶有更新不保证严格一致性
READ_WRITE经常更新软锁保证一致性
TRANSACTIONALJTA 环境XA 事务协调

3. MyBatis 核心机制

3.1 Mapper 接口绑定

public interface UserMapper {
    @Select("SELECT * FROM users WHERE id = #{id}")
    @Results({
        @Result(property = "createTime", column = "create_time"),
        @Result(property = "role", column = "role_id", 
                one = @One(select = "com.example.RoleMapper.selectById"))
    })
    User findById(Long id);
    
    @SelectProvider(type = UserSqlProvider.class, method = "findByCondition")
    List<User> findByCondition(UserQuery query);
}

动态 SQL(XML 方式)

<select id="findByCondition" resultType="User">
    SELECT * FROM users
    <where>
        <if test="name != null">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
        <choose>
            <when test="sortField != null">
                ORDER BY ${sortField} ${sortOrder}
            </when>
            <otherwise>
                ORDER BY id DESC
            </otherwise>
        </choose>
    </where>
</select>

3.2 结果集映射进阶

<resultMap id="orderResultMap" type="Order">
    <id property="id" column="order_id"/>
    <result property="amount" column="amount"/>
    <!-- 一对一 -->
    <association property="user" javaType="User">
        <id property="id" column="user_id"/>
        <result property="name" column="user_name"/>
    </association>
    <!-- 一对多 -->
    <collection property="items" ofType="OrderItem">
        <id property="id" column="item_id"/>
        <result property="productName" column="product_name"/>
    </collection>
</resultMap>

3.3 批量操作优化

方式 1:foreach 批量插入

<insert id="batchInsert">
    INSERT INTO users (name, email) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.email})
    </foreach>
</insert>

方式 2:ExecutorType.BATCH

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
    UserMapper mapper = session.getMapper(UserMapper.class);
    for (User user : users) {
        mapper.insert(user);  // 缓存 SQL,满 batch 才发送
    }
    session.commit();
} finally {
    session.close();
}

方式 3:MySQL rewriteBatchedStatements=true

jdbc.url=jdbc:mysql://localhost/db?rewriteBatchedStatements=true
# 将多条语句合并为 multi-value 插入,提升 3-5 倍性能
方式说明适用
foreach单条大 SQL,受 max_allowed_packet 限制中小批量
BATCH缓存累积,自动分批大批量
rewriteJDBC 驱动层优化MySQL 最优

4. Spring Data JPA 简化层

public interface UserRepository extends JpaRepository<User, Long>, 
                                        JpaSpecificationExecutor<User> {
    // 方法名派生查询
    List<User> findByNameContainingAndStatusOrderByCreateTimeDesc(String name, Integer status);
    
    // JPQL
    @Query("SELECT u FROM User u WHERE u.status = :status AND u.createTime > :since")
    List<User> findActiveUsersSince(@Param("status") Integer status, 
                                     @Param("since") LocalDateTime since);
    
    // 原生 SQL
    @Query(value = "SELECT * FROM users u WHERE JSON_CONTAINS(u.roles, ?1)", nativeQuery = true)
    List<User> findByRoleJson(String role);
    
    // 投影/聚合
    @Query("SELECT u.status, COUNT(u) FROM User u GROUP BY u.status")
    List<StatusCount> countByStatus();
}

Specification 动态条件

public static Specification<User> withFilter(String name, Integer status) {
    return (root, query, cb) -> {
        List<Predicate> predicates = new ArrayList<>();
        if (name != null) {
            predicates.add(cb.like(root.get("name"), "%" + name + "%"));
        }
        if (status != null) {
            predicates.add(cb.equal(root.get("status"), status));
        }
        return cb.and(predicates.toArray(new Predicate[0]));
    };
}

// 使用
userRepo.findAll(withFilter("test", 1), PageRequest.of(0, 20));

5. 性能优化清单

问题Hibernate 方案MyBatis 方案
N+1JOIN FETCH / Entity Graph / @BatchSize手写 JOIN 结果集映射
大分页setFirstResult + setMaxResults / 游标分页LIMIT + 覆盖索引
批量插入hibernate.jdbc.batch_size + 有序插入rewriteBatchedStatements / BATCH executor
连接池耗尽HikariCP 参数调优同上
二级缓存Ehcache / Caffeine / Redis自行集成 Caffeine / Redis
乐观锁@Versionversion 字段手动处理

5.1 HikariCP 最佳配置

spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.pool-name=PrimaryHikariPool

公式maximum-pool-size ≤ (CPU核数 × 2) + 有效磁盘数,或 (并发请求数 × 单次查询平均时间) / 最大容忍延迟

6. 选型决策树

新项目?
  ├─ 团队熟悉 SQL → MyBatis Plus / MyBatis Flex
  │                 └─ 需要完整 CRUD → MyBatis Plus
  └─ 团队偏 OO / DDD → JPA / Hibernate
        ├─ 需要复杂动态 SQL → JPA + QueryDSL / jOOQ
        └─ 标准 CRUD 为主 → Spring Data JPA

遗留系统?
  ├─ 已有手写 SQL → MyBatis 迁移成本最低
  └─ 存储过程密集 → 继续保持原生 JDBC / jOOQ

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析