Skip to content

JPA 基础

JPA 是 Java 持久化规范,Hibernate 是常见实现。它通过实体类映射数据库表,开发者更多操作对象,而不是直接拼接 SQL。

JPA 只定义规范,具体的持久化上下文、脏检查、SQL 生成、懒加载通常由 Hibernate 实现。完整过程见 Hibernate/JPA 核心全过程原理

为什么需要 JPA

JPA 解决的是“对象模型和关系表之间的映射问题”。没有 ORM 时,开发者需要手写 SQL、手动把 ResultSet 转成对象、手动维护新增和修改字段。

JPA 的目标是让开发者更多关注实体状态变化:

mermaid
flowchart TD
    A["Java Entity"] --> B["Persistence Context"]
    B --> C["脏检查"]
    C --> D["生成 SQL"]
    D --> E["同步到数据库"]

如果不用 JPA,也不是不行。MyBatis 更适合 SQL 可控性强的项目;JPA 更适合领域模型清晰、CRUD 较多、希望用对象状态驱动持久化的项目。

核心概念

概念说明
Entity实体类,对应数据库表
EntityManager实体管理器,负责持久化操作
Persistence Context持久化上下文,保存实体状态
JPQL面向对象的查询语言
RepositorySpring Data JPA 中的数据访问接口

实体状态

mermaid
flowchart TD
    A[Transient 临时态] --> B[persist]
    B --> C[Managed 托管态]
    C --> D[detach]
    D --> E[Detached 游离态]
    C --> F[remove]
    F --> G[Removed 删除态]

工作原理

JPA 的核心是持久化上下文。事务中查询或保存的实体会进入托管态,JPA 会记录实体快照;事务提交时,如果托管实体字段发生变化,就通过脏检查生成更新 SQL。

mermaid
flowchart TD
    A["find 查询实体"] --> B["实体进入 Managed 状态"]
    B --> C["业务代码修改字段"]
    C --> D["事务提交"]
    D --> E["JPA 脏检查"]
    E --> F["生成 update SQL"]

这也是为什么 JPA 不需要每次修改后显式调用 update。但如果实体已经离开事务或持久化上下文,就会变成游离态,修改字段不会自动同步到数据库。

优点与注意点

项目说明
优点CRUD 简洁、对象模型友好、适合领域模型
注意复杂 SQL 可控性不如 MyBatis
风险N+1 查询、懒加载异常、事务边界不清

开发建议

  1. 简单 CRUD 使用 Repository,复杂查询可以用 JPQL、Specification 或原生 SQL。
  2. 关联关系不要滥用双向映射,容易造成序列化循环和加载过多。
  3. 列表接口要明确分页,避免一次加载大量实体。
  4. 事务范围内访问懒加载字段,避免 LazyInitializationException

JPA 不是“自动帮你省掉 SQL 思考”

JPA 会让代码更像操作对象,但数据库最终执行的仍然是 SQL。你必须同时关注两层:

mermaid
flowchart TD
    A["Java Entity 状态变化"] --> B["持久化上下文"]
    B --> C["Hibernate 生成 SQL"]
    C --> D["数据库执行计划"]
    D --> E["索引、锁、事务、IO"]

如果只看 Java 代码,不看最终 SQL,容易出现:

  1. N+1 查询。
  2. 懒加载异常。
  3. 批量更新变成大量单条 SQL。
  4. 分页 count 很慢。
  5. 实体直接返回造成循环引用和字段泄露。

Entity 映射 Demo

java
@Entity
@Table(name = "asset")
public class Asset {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "asset_code", nullable = false, unique = true, length = 64)
    private String assetCode;

    @Column(name = "asset_name", nullable = false, length = 128)
    private String assetName;

    @Column(name = "status", nullable = false)
    private Integer status;

    @Column(name = "created_at", nullable = false)
    private LocalDateTime createdAt;

    protected Asset() {
    }

    public Asset(String assetCode, String assetName) {
        this.assetCode = assetCode;
        this.assetName = assetName;
        this.status = 1;
        this.createdAt = LocalDateTime.now();
    }

    public void changeName(String assetName) {
        if (assetName == null || assetName.isBlank()) {
            throw new IllegalArgumentException("资产名称不能为空");
        }
        this.assetName = assetName;
    }
}

关键点:

  1. @Entity 表示这是持久化实体。
  2. @Table 指定表名。
  3. @Id 是主键。
  4. @GeneratedValue 指定主键生成策略。
  5. @Column 描述列名、非空、长度、唯一性等。
  6. 受保护无参构造给 JPA 反射创建对象使用。
  7. 业务修改通过方法收敛,不建议所有字段无脑暴露 setter。

主键生成策略怎么选

策略含义适用
IDENTITY数据库自增MySQL、SQL Server 常见
SEQUENCE数据库序列Oracle、PostgreSQL 常见
TABLE用表模拟序列很少用,性能一般
AUTO由 Provider 根据数据库选择简单但可控性弱

主键策略会影响插入时机和批量性能。例如 MySQL IDENTITY 通常要执行 insert 后才能拿到主键,批量插入优化空间可能受影响;Oracle/PostgreSQL sequence 可以提前拿号。

持久化上下文是什么

持久化上下文可以理解为一个“事务内实体管理区”。它保存:

  1. 当前托管实体。
  2. 实体快照。
  3. 待插入、待更新、待删除动作。
  4. 一级缓存。
mermaid
flowchart TD
    A["EntityManager.find"] --> B["查询数据库"]
    B --> C["创建 Entity"]
    C --> D["放入 Persistence Context"]
    D --> E["保存快照"]
    E --> F["返回托管态对象"]

同一个事务内再次按主键查询同一个实体,可能直接从持久化上下文返回,而不是再查数据库。

实体状态详解

状态怎么产生是否自动同步数据库
Transient 临时态new Asset()
Managed 托管态persistfind、查询返回是,flush 时脏检查
Detached 游离态事务结束、detach、序列化出上下文
Removed 删除态removecommit 时删除

状态流转:

mermaid
flowchart TD
    A["new Entity<br/>临时态"] --> B["persist"]
    B --> C["托管态 Managed"]
    C --> D["业务修改字段"]
    D --> E["flush 脏检查生成 update"]
    C --> F["detach/事务结束"]
    F --> G["游离态 Detached"]
    G --> H["merge"]
    H --> C
    C --> I["remove"]
    I --> J["删除态 Removed"]

为什么修改实体不用显式 update

事务内查询出来的实体是托管态,Hibernate 会保存快照。

java
@Transactional
public void changeAssetName(Long id, String name) {
    Asset asset = entityManager.find(Asset.class, id);
    asset.changeName(name);
}

执行过程:

mermaid
flowchart TD
    A["find 查询 Asset"] --> B["进入托管态并保存快照"]
    B --> C["changeName 修改字段"]
    C --> D["事务提交前 flush"]
    D --> E["当前值和快照比较"]
    E --> F{"是否变化"}
    F -- "是" --> G["生成 update SQL"]
    F -- "否" --> H["不发 update"]

这就是脏检查。它不是魔法,而是“托管实体 + 快照对比 + flush”。

flush 和 commit 区别

概念含义
flush把持久化上下文里的变化转换成 SQL 发给数据库
commit提交数据库事务
mermaid
flowchart TD
    A["修改托管实体"] --> B["flush"]
    B --> C["发送 insert/update/delete SQL"]
    C --> D{"事务是否继续"}
    D -- "继续" --> E["SQL 已发出但可回滚"]
    D -- "提交" --> F["commit"]
    D -- "异常" --> G["rollback"]

flush 后 SQL 已经发给数据库,但事务还没提交,仍然可以回滚。commit 通常会触发 flush,但 flush 不等于 commit。

merge 为什么要谨慎

merge 会把游离对象的状态合并到一个托管对象上。

java
@Transactional
public void updateFromRequest(Asset requestAsset) {
    entityManager.merge(requestAsset);
}

风险:如果前端只传了 idassetName,其他字段是 null,merge 可能把 null 也合并进去,造成字段被覆盖。

更推荐:

java
@Transactional
public void changeAssetName(Long id, String name) {
    Asset asset = entityManager.find(Asset.class, id);
    asset.changeName(name);
}

先查托管实体,再调用有业务含义的方法修改。这能避免“请求 DTO 覆盖实体”的风险。

懒加载为什么会失败

懒加载字段不是在查询主实体时立即加载,而是在访问关联属性时再查。

java
Asset asset = assetRepository.findById(id).orElseThrow();
return asset.getDepartment().getName();

如果访问 department 时事务和持久化上下文已经关闭,就可能报 LazyInitializationException

mermaid
flowchart TD
    A["查询 Asset"] --> B["department 是代理对象"]
    B --> C["事务结束,EntityManager 关闭"]
    C --> D["Controller 序列化访问 department"]
    D --> E["没有上下文,懒加载失败"]

解决:

  1. Service 层事务内转 DTO。
  2. 使用 fetch join 或 EntityGraph 明确加载需要的关联。
  3. 列表页直接 DTO 投影。
  4. 不要把 Entity 直接返回 Controller。

N+1 查询为什么出现

java
List<Asset> assets = assetRepository.findByStatus(1);
for (Asset asset : assets) {
    String deptName = asset.getDepartment().getName();
}

如果列表有 100 条,可能先查 1 次资产,再查 100 次科室。

mermaid
flowchart TD
    A["查询资产列表 1 次"] --> B["得到 N 个 Asset"]
    B --> C["循环访问 department"]
    C --> D["触发 N 次额外查询"]

解决:

  1. DTO 查询一次拿需要字段。
  2. fetch join。
  3. EntityGraph。
  4. 批量查询关联数据再组装。

商业 Demo:资产详情 DTO

Repository:

java
public interface AssetRepository extends JpaRepository<Asset, Long> {
    @Query("""
        select new com.demo.AssetDetailDTO(
            a.id, a.assetCode, a.assetName, d.deptName
        )
        from Asset a
        join a.department d
        where a.id = :id
    """)
    Optional<AssetDetailDTO> findDetail(@Param("id") Long id);
}

DTO:

java
public record AssetDetailDTO(
    Long id,
    String assetCode,
    String assetName,
    String deptName
) {
}

为什么用 DTO:

  1. 避免返回 Entity 暴露字段。
  2. 避免接口序列化触发懒加载。
  3. 避免循环引用。
  4. SQL 只查询接口需要的字段。

常见坑

问题原因建议
修改实体不生效实体是游离态或无事务在事务内查询托管实体后修改
字段被 null 覆盖直接 merge 请求对象DTO 入参,先查实体再改
懒加载异常事务外访问关联Service 内转 DTO
N+1 查询列表循环访问懒加载关联DTO、fetch join、EntityGraph
返回 Entity字段泄露、循环引用、隐式 SQL返回 DTO
事务过大锁持有久、连接占用久缩小事务范围

面试标准回答

text
JPA 是持久化规范,Hibernate 是常见实现。JPA 的核心是 EntityManager 和持久化上下文。事务内通过 find 或查询拿到的实体是托管态,持久化上下文会保存实体快照;提交前 flush 时 Hibernate 做脏检查,发现字段变化就生成 update SQL。flush 是把变化同步成 SQL,commit 是提交数据库事务。JPA 使用时要重点关注实体状态、merge 覆盖风险、懒加载异常、N+1 查询、事务边界和 DTO 返回,不能只看 Java 对象操作而忽略最终 SQL。

代码 Demo:EntityManager 基本操作

java
@Service
public class JpaUserService {
    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public Long createUser(String username) {
        User user = new User();
        user.setUsername(username);
        entityManager.persist(user);
        return user.getId();
    }

    @Transactional(readOnly = true)
    public User findUser(Long id) {
        return entityManager.find(User.class, id);
    }

    @Transactional
    public void changeUsername(Long id, String username) {
        User user = entityManager.find(User.class, id);
        user.setUsername(username);
        // 托管态实体会在事务提交时通过脏检查自动更新。
    }
}

这个 Demo 展示了 JPA 的核心:persist 让临时态实体变成托管态,find 查询实体,托管态实体字段变化会在事务提交时同步到数据库。

如果想继续理解为什么 flush 不等于 commit、为什么 merge 可能覆盖空字段、为什么懒加载会失败,请继续看 Hibernate/JPA 核心全过程原理