JPA 基础
JPA 是 Java 持久化规范,Hibernate 是常见实现。它通过实体类映射数据库表,开发者更多操作对象,而不是直接拼接 SQL。
JPA 只定义规范,具体的持久化上下文、脏检查、SQL 生成、懒加载通常由 Hibernate 实现。完整过程见 Hibernate/JPA 核心全过程原理。
为什么需要 JPA
JPA 解决的是“对象模型和关系表之间的映射问题”。没有 ORM 时,开发者需要手写 SQL、手动把 ResultSet 转成对象、手动维护新增和修改字段。
JPA 的目标是让开发者更多关注实体状态变化:
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 | 面向对象的查询语言 |
| Repository | Spring Data JPA 中的数据访问接口 |
实体状态
flowchart TD
A[Transient 临时态] --> B[persist]
B --> C[Managed 托管态]
C --> D[detach]
D --> E[Detached 游离态]
C --> F[remove]
F --> G[Removed 删除态]工作原理
JPA 的核心是持久化上下文。事务中查询或保存的实体会进入托管态,JPA 会记录实体快照;事务提交时,如果托管实体字段发生变化,就通过脏检查生成更新 SQL。
flowchart TD
A["find 查询实体"] --> B["实体进入 Managed 状态"]
B --> C["业务代码修改字段"]
C --> D["事务提交"]
D --> E["JPA 脏检查"]
E --> F["生成 update SQL"]这也是为什么 JPA 不需要每次修改后显式调用 update。但如果实体已经离开事务或持久化上下文,就会变成游离态,修改字段不会自动同步到数据库。
优点与注意点
| 项目 | 说明 |
|---|---|
| 优点 | CRUD 简洁、对象模型友好、适合领域模型 |
| 注意 | 复杂 SQL 可控性不如 MyBatis |
| 风险 | N+1 查询、懒加载异常、事务边界不清 |
开发建议
- 简单 CRUD 使用 Repository,复杂查询可以用 JPQL、Specification 或原生 SQL。
- 关联关系不要滥用双向映射,容易造成序列化循环和加载过多。
- 列表接口要明确分页,避免一次加载大量实体。
- 事务范围内访问懒加载字段,避免
LazyInitializationException。
JPA 不是“自动帮你省掉 SQL 思考”
JPA 会让代码更像操作对象,但数据库最终执行的仍然是 SQL。你必须同时关注两层:
flowchart TD
A["Java Entity 状态变化"] --> B["持久化上下文"]
B --> C["Hibernate 生成 SQL"]
C --> D["数据库执行计划"]
D --> E["索引、锁、事务、IO"]如果只看 Java 代码,不看最终 SQL,容易出现:
- N+1 查询。
- 懒加载异常。
- 批量更新变成大量单条 SQL。
- 分页 count 很慢。
- 实体直接返回造成循环引用和字段泄露。
Entity 映射 Demo
@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;
}
}关键点:
@Entity表示这是持久化实体。@Table指定表名。@Id是主键。@GeneratedValue指定主键生成策略。@Column描述列名、非空、长度、唯一性等。- 受保护无参构造给 JPA 反射创建对象使用。
- 业务修改通过方法收敛,不建议所有字段无脑暴露 setter。
主键生成策略怎么选
| 策略 | 含义 | 适用 |
|---|---|---|
IDENTITY | 数据库自增 | MySQL、SQL Server 常见 |
SEQUENCE | 数据库序列 | Oracle、PostgreSQL 常见 |
TABLE | 用表模拟序列 | 很少用,性能一般 |
AUTO | 由 Provider 根据数据库选择 | 简单但可控性弱 |
主键策略会影响插入时机和批量性能。例如 MySQL IDENTITY 通常要执行 insert 后才能拿到主键,批量插入优化空间可能受影响;Oracle/PostgreSQL sequence 可以提前拿号。
持久化上下文是什么
持久化上下文可以理解为一个“事务内实体管理区”。它保存:
- 当前托管实体。
- 实体快照。
- 待插入、待更新、待删除动作。
- 一级缓存。
flowchart TD
A["EntityManager.find"] --> B["查询数据库"]
B --> C["创建 Entity"]
C --> D["放入 Persistence Context"]
D --> E["保存快照"]
E --> F["返回托管态对象"]同一个事务内再次按主键查询同一个实体,可能直接从持久化上下文返回,而不是再查数据库。
实体状态详解
| 状态 | 怎么产生 | 是否自动同步数据库 |
|---|---|---|
| Transient 临时态 | new Asset() | 否 |
| Managed 托管态 | persist、find、查询返回 | 是,flush 时脏检查 |
| Detached 游离态 | 事务结束、detach、序列化出上下文 | 否 |
| Removed 删除态 | remove | commit 时删除 |
状态流转:
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 会保存快照。
@Transactional
public void changeAssetName(Long id, String name) {
Asset asset = entityManager.find(Asset.class, id);
asset.changeName(name);
}执行过程:
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 | 提交数据库事务 |
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 会把游离对象的状态合并到一个托管对象上。
@Transactional
public void updateFromRequest(Asset requestAsset) {
entityManager.merge(requestAsset);
}风险:如果前端只传了 id 和 assetName,其他字段是 null,merge 可能把 null 也合并进去,造成字段被覆盖。
更推荐:
@Transactional
public void changeAssetName(Long id, String name) {
Asset asset = entityManager.find(Asset.class, id);
asset.changeName(name);
}先查托管实体,再调用有业务含义的方法修改。这能避免“请求 DTO 覆盖实体”的风险。
懒加载为什么会失败
懒加载字段不是在查询主实体时立即加载,而是在访问关联属性时再查。
Asset asset = assetRepository.findById(id).orElseThrow();
return asset.getDepartment().getName();如果访问 department 时事务和持久化上下文已经关闭,就可能报 LazyInitializationException。
flowchart TD
A["查询 Asset"] --> B["department 是代理对象"]
B --> C["事务结束,EntityManager 关闭"]
C --> D["Controller 序列化访问 department"]
D --> E["没有上下文,懒加载失败"]解决:
- Service 层事务内转 DTO。
- 使用 fetch join 或 EntityGraph 明确加载需要的关联。
- 列表页直接 DTO 投影。
- 不要把 Entity 直接返回 Controller。
N+1 查询为什么出现
List<Asset> assets = assetRepository.findByStatus(1);
for (Asset asset : assets) {
String deptName = asset.getDepartment().getName();
}如果列表有 100 条,可能先查 1 次资产,再查 100 次科室。
flowchart TD
A["查询资产列表 1 次"] --> B["得到 N 个 Asset"]
B --> C["循环访问 department"]
C --> D["触发 N 次额外查询"]解决:
- DTO 查询一次拿需要字段。
- fetch join。
- EntityGraph。
- 批量查询关联数据再组装。
商业 Demo:资产详情 DTO
Repository:
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:
public record AssetDetailDTO(
Long id,
String assetCode,
String assetName,
String deptName
) {
}为什么用 DTO:
- 避免返回 Entity 暴露字段。
- 避免接口序列化触发懒加载。
- 避免循环引用。
- SQL 只查询接口需要的字段。
常见坑
| 问题 | 原因 | 建议 |
|---|---|---|
| 修改实体不生效 | 实体是游离态或无事务 | 在事务内查询托管实体后修改 |
| 字段被 null 覆盖 | 直接 merge 请求对象 | DTO 入参,先查实体再改 |
| 懒加载异常 | 事务外访问关联 | Service 内转 DTO |
| N+1 查询 | 列表循环访问懒加载关联 | DTO、fetch join、EntityGraph |
| 返回 Entity | 字段泄露、循环引用、隐式 SQL | 返回 DTO |
| 事务过大 | 锁持有久、连接占用久 | 缩小事务范围 |
面试标准回答
JPA 是持久化规范,Hibernate 是常见实现。JPA 的核心是 EntityManager 和持久化上下文。事务内通过 find 或查询拿到的实体是托管态,持久化上下文会保存实体快照;提交前 flush 时 Hibernate 做脏检查,发现字段变化就生成 update SQL。flush 是把变化同步成 SQL,commit 是提交数据库事务。JPA 使用时要重点关注实体状态、merge 覆盖风险、懒加载异常、N+1 查询、事务边界和 DTO 返回,不能只看 Java 对象操作而忽略最终 SQL。代码 Demo:EntityManager 基本操作
@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 核心全过程原理。
