JPA、Hibernate 与 Spring Data:对象怎样同步到数据库
order.setAmountCents(700) 修改的是一个 Java 对象。这个对象如果由当前 EntityManager 管理,Hibernate 会在同步时检查变化,生成 UPDATE;如果对象已经脱离持久化上下文,相同 setter 只改变内存。
理解 ORM,需要同时看三样具体内容:哪个 EntityManager 持有对象,实体映射对应哪些表和列,当前事务走到了哪里。Repository 提供了更简洁的调用入口,这些底层关系仍然存在。
规范、实现与 Repository 分别做什么
一次调用经过的对象
Jakarta Persistence 定义 EntityManager、实体映射、持久化上下文和锁等契约;Hibernate ORM 是实现这些契约的 provider。Spring Data JPA 为 Repository 接口创建代理,提供常见 CRUD、方法名查询和查询方法装配。Spring 的 JpaTransactionManager 则负责本地 JPA 事务及 EntityManager 的绑定。
应用服务方法
└─ Spring Data Repository 代理
└─ EntityManager 接口
└─ Hibernate Session / 持久化上下文
└─ JDBC Connection → PostgreSQL只使用 EntityManager 也可以完成持久化,不必先建立 Repository。反过来,添加 Repository 接口不会替代实体映射和事务设计。Jakarta Persistence 3.2、Spring JPA 集成
EntityManagerFactory 保存一个持久化单元的映射和运行基础设施,通常长期复用。EntityManager 对应一次工作单元,持有实体和待同步的变化,不应由多个线程并发使用。
把映射放回关系表中
订单和明细可以映射为:
purchase_order
├─ id:订单主键
├─ version:乐观锁版本
├─ external_key:业务唯一键
├─ title
└─ amount_cents
order_line
├─ id:明细主键
├─ order_id → purchase_order.id
└─ sku实体类上的注解描述 Java 属性与这些数据库对象的关系。数据库中的主键、外键、唯一约束仍负责约束最终数据;生产环境的表结构需要通过受控迁移建立,不能仅依赖应用启动时自动改表。
@Id 放在字段上时,默认采用字段访问;放在 getter 上时,默认采用属性访问。两种方式混用时应使用明确的 @Access,否则容易让“字段存在但映射未生效”的问题藏在类结构中。
用完整工程保存并读取订单
版本和运行环境
下载完整实验 ZIP。核心文件包括 PurchaseOrder.java、OrderLine.java、persistence.xml、Factories.java 和 JpaLab.java。
工程导入 Spring Boot 4.1.1 的依赖清单,实际使用 Hibernate ORM 7.4.5.Final、Spring Data JPA 4.1.1、Jakarta Persistence 3.2 和 pgJDBC 42.7.13。数据库为 PostgreSQL 18.6,默认 Maven 3.9.12 / JDK 25,源码编译目标 Java 17。Boot 依赖版本
这里使用普通 main 方法建立资源本地事务,便于直接观察 EntityManager。Boot 4.1.1 应用至少需要 Java 17,其框架版本要求见 系统要求。Hibernate 7.4 在线手册可能展示更新补丁;下面的输出对应工程固定依赖。
在 Linux Bash 中,以能够运行 Docker 的普通用户执行:
test "$(id -u)" -ne 0 || exit 1
docker version
docker compose version
unzip jpa-hibernate-lab.zip
cd jpa-hibernate
export LAB_DIR="$PWD"
export LAB_CACHE="$LAB_DIR/.m2-cache"
mkdir -p "$LAB_CACHE"
export LAB_DB_PASSWORD="$(openssl rand -hex 24)"
export LAB_APP_PASSWORD="$(openssl rand -hex 24)"
export LAB_DB_URL='jdbc:postgresql://db:5432/jpa_lab'
docker compose -p da10-jpa up -d --wait需要预先安装 Docker Engine、Compose、unzip、openssl。数据库容器进程使用 UID/GID 999:999,应用使用宿主普通用户身份。数据库角色 lab_owner 创建表和序列,Java 使用普通角色 lab_app;应用没有建表权限。
数据库没有映射宿主端口,数据目录为 tmpfs。init.sh 准备两条订单和一条明细,各实验模式会重置这几张实验表。不要把 LAB_DB_URL 改成业务数据库。
定义调用函数:
run_lab() {
docker run --rm --user "$(id -u):$(id -g)" --read-only \
--network da10-jpa_default --tmpfs /tmp:rw,exec,mode=1777 \
-v "$LAB_DIR:/src:ro" -v "$LAB_CACHE:/cache" \
-e LAB_DB_URL -e LAB_APP_PASSWORD -e MAVEN_CONFIG=/tmp/maven \
maven:3.9.12-eclipse-temurin-25 bash /src/run.sh "$1"
}run.sh 把构建输出放进容器 /tmp,执行结束后清理;源码挂载为只读,Maven 缓存留在解压目录。第一次下载依赖可能较慢。
Entity 与主键序列
订单实体的关键映射如下,完整 getter 和关联方法见源文件:
@Entity
@Table(name = "purchase_order")
public class PurchaseOrder {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_ids")
@SequenceGenerator(
name = "order_ids", sequenceName = "order_seq", allocationSize = 1)
private Long id;
@Version
private Long version;
@Column(name = "external_key", nullable = false, unique = true, length = 80)
private String externalKey;
@Column(nullable = false, length = 120)
private String title;
@Column(name = "amount_cents", nullable = false)
private long amountCents;
@OneToMany(mappedBy = "order",
cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderLine> lines = new ArrayList<>();
protected PurchaseOrder() {}
}实体保留无参构造器,使用非 final 类,便于 provider 按映射建立对象和代理。这里用 Long 表示尚未生成的主键和版本;不要把数据库 ID 是否为零与实体是否新建混成一个判断。
SEQUENCE 可以先从数据库序列获取 ID,再等待合适时机执行 INSERT。实验将 allocationSize 设为 1,与数据库序列步长一致,方便观察;高吞吐写入可以评估预分配,但必须让 ORM 生成策略与数据库序列配置相容。
IDENTITY 依赖 INSERT 后返回数据库生成值,通常会更早触发插入,也会影响 Hibernate 的插入批处理。主键在 Java 对象中出现,不表示事务已经提交。各种生成策略见 Hibernate 标识符。
配置持久化单元
META-INF/persistence.xml 声明 provider、实体类和事务类型:
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.2">
<persistence-unit name="lab" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>example.PurchaseOrder</class>
<class>example.OrderLine</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="hibernate.hbm2ddl.auto" value="validate"/>
<property name="hibernate.generate_statistics" value="true"/>
<property name="hibernate.cache.use_second_level_cache" value="false"/>
<property name="hibernate.cache.use_query_cache" value="false"/>
<property name="hibernate.jdbc.batch_size" value="20"/>
</properties>
</persistence-unit>
</persistence>validate 检查映射与现有结构是否匹配,不创建表。工厂从环境变量读取数据库地址和密码,建立 PGSimpleDataSource,再作为 jakarta.persistence.nonJtaDataSource 传入:
EntityManagerFactory factory = Persistence.createEntityManagerFactory(
"lab", Map.of("jakarta.persistence.nonJtaDataSource", dataSource));本实验没有连接池,日志显示 DataSourceConnectionProvider。实际服务可以注入受管理的池化 DataSource,连接借还与复位机制见 JDBC 与连接池。
首次保存和读取
运行:
run_lab first关键输出:
persisted=true amountCents=1250 sameContextSameObject=true cascadeLineRows=1保存使用一个明确事务:
var order = new PurchaseOrder("first", "created", 1250);
order.addLine("sku-first");
try (var em = factory.createEntityManager()) {
var tx = em.getTransaction();
tx.begin();
try {
em.persist(order);
tx.commit();
} catch (RuntimeException | Error failure) {
if (tx.isActive()) {
try { tx.rollback(); }
catch (RuntimeException rollbackFailure) {
failure.addSuppressed(rollbackFailure);
}
}
throw failure;
}
}代码用 try-with-resources 关闭 EntityManager。主程序也在所有模式结束后关闭 EntityManagerFactory。回滚异常以 suppressed 形式保留,避免覆盖最初失败原因。
保存之后,新 EntityManager 按相同 ID 连续 find 两次,验证 first == second;读取明细集合得到一行。每个模式最后都会恢复初始数据,所以模式结束后不能再按刚生成的 ID 查询它。
如果在输出之前出现 Schema-validation 错误,先检查数据库日志和实际表结构,而不是把 hbm2ddl.auto 改成 update:
docker compose -p da10-jpa logs --tail=80 db
docker compose -p da10-jpa exec -T db \
psql -X -U lab_owner -d jpa_lab -v ON_ERROR_STOP=1 \
-c "\d purchase_order"持久化上下文怎样决定 SQL
受管对象、快照和待执行动作
同一持久化上下文通过实体类型和主键查找受管实例。find 已有实例时可以直接返回;这也意味着另一个连接更新数据库后,再次 find 未必重新查询。
Hibernate 对普通可变实体保存管理信息和用于变化比较的状态。flush 时,脏检查识别需要更新的属性,并把相应工作交给执行队列。启用字节码增强后,还可以使用属性变化跟踪;未增强的普通实体不能直接套用那种实现解释。Hibernate 持久化上下文
EntityManager / Hibernate Session
├─ 实体标识 → 当前受管对象
├─ EntityEntry 与已加载状态
├─ 集合包装与关联状态
└─ ActionQueue → 待执行的实体/集合动作一个长时间不关闭的上下文会持续持有实体和相关状态。大批量处理可以定期 flush 后 clear,控制内存;clear 会让当前所有受管对象脱离管理,后续继续改旧引用时要意识到这个变化。
flush 先同步当前事务
运行:
run_lab flushdirtyUpdateBeforeFlush=0 afterFlush=1 externalBeforeCommit=500 externalAfterCommit=700
flushedUpdateRolledBack=true amountCents=700初始金额为 500。实验加载对象后改成 700,Hibernate Statistics 的实体更新计数仍为 0;调用 flush 后计数变为 1,独立 JDBC 连接仍读到 500;提交后才读到 700。
第二个事务把金额改为 900,flush 后回滚,独立连接仍读到 700。SQL 已执行的变化仍处于事务中,回滚可以撤销这次更新。
这里统计的是 Hibernate 的实体更新事件,同时用真实数据库读值交叉判断,未把 SQL 日志条数直接当作提交次数。Statistics 属于工厂范围;并发应用中的全局计数还会包含其他请求,测试时应隔离工作负载。
查询也可能触发同步
默认 AUTO flush 模式下,Hibernate 会在需要时于事务提交前、相关查询前同步待执行变化,使查询能反映符合要求的当前状态。具体触发还与 JPQL/HQL、原生 SQL、查询空间以及使用的 API 有关。
COMMIT 模式减少查询前的同步要求,但事务提交仍要处理变化。把 flush 模式改成 COMMIT 后,依赖“先改字段再查询统计值”的代码需要重新检查其语义。
调用 flush 是要求同步,不是创建新事务;没有活动事务时执行要求事务的写入会失败。自动同步、显式同步和 SQL 排序见 Hibernate Flushing。
Java 调用顺序与 SQL 顺序
Hibernate 的 ActionQueue 按动作类别组织执行,SQL 顺序可能与应用方法调用顺序不同。一个很直接的反例是先删除旧对象,再新增相同业务唯一键:
em.remove(em.find(PurchaseOrder.class, 2L));
em.persist(new PurchaseOrder("replace", "new", 200));
em.flush();运行:
run_lab orderinginsertBeforeDeleteUniqueRejected=true sqlState=23505 explicitDeleteFlushRepaired=true数据库中 id=2 已持有 external_key=replace。这一组操作 flush 时先尝试 INSERT,旧行还在,于是违反唯一约束。实验回滚后检查旧行仍存在。
下一事务先 remove 并 flush 删除,再 persist 新对象,最终成功。这个修复适合确实需要“删除后重新创建”的业务;如果目的只是修改几个字段,直接更新已有受管实体通常更自然,也能保持主键和关联关系。
显式 flush 改变发送时点,同时可能提前获得锁或触发约束错误,应放在有具体顺序需求的位置。
实体离开上下文以后怎样更新
四种状态与常见操作
| 状态 | EntityManager 是否管理 | 常见进入方式 | 后续字段修改 |
|---|---|---|---|
| 新对象 | 否 | Java 构造器创建 | 只修改内存 |
| managed | 是 | persist、find、查询、merge 返回值 | 可以被脏检查识别 |
| detached | 否 | detach、clear、关闭上下文 | 只修改原对象内存 |
| removed | 仍由上下文跟踪删除动作 | remove 受管对象 | 重点是待执行删除,不再按普通更新使用 |
getReference 可以先提供引用或代理,需要访问数据时才初始化;调用它不适合作为“数据库一定存在该行”的存在性检查。refresh 则重新加载数据库状态,可能覆盖尚未同步的本地修改。
merge 复制状态,不改变原对象身份
运行:
run_lab mergemergeReturnedManagedCopy=true originalRemainedDetached=true savedAmountCents=650实验先关闭读取对象的 EntityManager,再修改 detached 对象:
detached.setAmountCents(650);
PurchaseOrder managed = em.merge(detached);
detached.setAmountCents(999); // 后续改的是旧对象。merge 把状态复制到受管实例并返回它,原 detached 对象仍未被这个 EntityManager 管理。提交后数据库保存 650,没有采用后来写在旧对象上的 999。
detached 对象 ──复制当前属性──→ managed 对象
│ │
后续 setter 只改旧引用 脏检查 / flush
│
▼
当前数据库事务合并一个完整的外部对象还可能把调用方未意识到的字段覆盖回去。HTTP 更新接口更常用的做法是:在事务内按 ID 加载实体,检查权限和版本,再只修改请求允许更新的属性。这样可以避免把缺省字段、关联集合和客户端提交的主键一并当作可信状态。
bulk JPQL 跳过已加载对象
运行:
run_lab bulkbulkRows=1 loadedObjectStayedOld=true clearThenFresh=true versionUnchanged=true实验先加载金额 500 的实体,再执行:
em.createQuery(
"update PurchaseOrder o set o.amountCents=800 where o.id=1")
.executeUpdate();已加载对象仍保存旧值,clear 后再次 find 才看到 800。这个普通 bulk update 也没有自动增加 version。
批量 JPQL/native SQL 适合直接操作一批行,但不按逐实体 setter 的方式更新上下文。使用前要处理待同步变化,执行后决定 clear、refresh 或新的工作单元;若需要乐观并发控制,应在批量语句中明确设计版本条件和版本更新。不要假设 @Version 自动覆盖任意原生 SQL。
关联映射怎样影响读取和删除
谁维护外键
OrderLine 的映射负责实际外键列:
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "order_id", nullable = false)
private PurchaseOrder order;PurchaseOrder 的 mappedBy="order" 指向这个 Java 属性,表示集合是关联的反向视图。给父对象的 List 添加元素时,同时设置子对象的 order,才能让两个 Java 方向一致。
完整工程的 addLine 创建带父对象的明细,再加入集合;removeLine 从集合移除并解除子对象引用。维护两侧是业务对象的方法职责,不应依赖每个调用者记住两次 setter。
cascade=PERSIST 等级联操作沿对象关联传播持久化动作;数据库 ON DELETE CASCADE 是数据库约束行为,两者作用位置不同。跨聚合、可独立存在的对象通常不适合随便级联 REMOVE。关联映射和生命周期选项见 Hibernate Associations。
orphanRemoval 删除脱离父对象的明细
运行:
run_lab orphancollectionRemovalDeletedOrphan=true remainingLines=0本模型中的明细不能脱离订单独立存在。事务内从受管集合移除唯一明细,提交后独立 JDBC 查询确认对应行已删除。
对于可共享的标签、用户、产品等对象,移除某个集合成员往往只应移除关联记录,而非删除对象本身。映射前先确定表中的外键或连接表表达的是“所属”还是“引用”。
懒加载需要仍可工作的上下文
运行:
run_lab lazyclosedContextLazyRejected=true fetchJoinUsableAfterClose=true lines=1第一组读取订单但不访问 lines,关闭上下文后调用集合 size,得到 LazyInitializationException。第二组用明确抓取计划加载订单和明细:
PurchaseOrder order = em.createQuery(
"select o from PurchaseOrder o left join fetch o.lines where o.id=1",
PurchaseOrder.class).getSingleResult();集合在上下文关闭前已初始化,之后读取这组已抓取数据可以成功。更严格的 Web 接口可以在事务内生成 DTO,只返回需要的字段。
JPA 默认把 to-one 关联配置为 EAGER、to-many 配置为 LAZY;LAZY 在规范层属于提示,provider 的代理和增强能力会影响具体行为。EAGER 要求及时可用,不意味着必须用一条 join SQL 完成。把所有关系改成 EAGER,可能把异常换成大量隐式查询。
fetch join、EntityGraph、投影和批量抓取适用于不同返回形状。集合 join 会扩展行数,分页时还可能产生内存截取问题,详见分页、N+1 与锁;抓取策略定义可查 Hibernate Fetching。
值对象、继承与数据库设计
@Embeddable 表示随所属实体存在的值对象,例如地址的多个字段可以展开到订单表;它没有独立实体身份。集合值可以用 ElementCollection 保存到附属表,需要考虑删除重建集合时的 SQL 规模。
继承策略要与查询方式共同选择:
| 策略 | 表结构 | 主要成本 |
|---|---|---|
| SINGLE_TABLE | 一个表加类型区分列 | 子类型字段可能形成较多空列与约束限制 |
| JOINED | 基类和子类各自表,通过主键关联 | 多态加载需要更多连接 |
| TABLE_PER_CLASS | 具体类型各自保存完整列 | 多态查询可能需要 UNION |
| MappedSuperclass | 父类属性复用到子实体映射 | 父类本身不是独立可查询实体 |
这些选项不会替代正常的关系建模。一个实体加载会产生哪些 SQL,可以通过抓取计划、Statistics 和数据库执行计划共同检查,而不是仅依据注解数量判断“对象设计是否完整”。Hibernate 继承映射
Spring Data 的 save 与事务入口
新实体走 persist,已有实体走 merge
Spring Data JPA 的 save 根据实体新旧判断选择 persist 或 merge。默认优先检查非原始类型的 version 属性,再检查 ID;实现 Persistable 时可以自定义 isNew 判断。Spring Data 实体持久化
本工程的 OrderRepository.java 很小:
public interface OrderRepository
extends JpaRepository<PurchaseOrder, Long> {
}实验在一个已经开启的资源本地事务中创建真实 JpaRepositoryFactory:
OrderRepository repository =
new JpaRepositoryFactory(em).getRepository(OrderRepository.class);
PurchaseOrder saved = repository.save(order);运行:
run_lab repositoryrepositoryNewSaveSameObject=true existingSaveManagedCopy=true savedAmountCents=350新订单 save 返回原对象,当前 EntityManager 管理它。关闭该上下文后,再次 save detached 订单返回受管副本;后来修改原引用为 999 不影响本次保存的 350。接收 save 返回值,才能继续操作正确对象。
saveAndFlush 增加同步动作,事务提交仍由外部事务入口决定。对于已经受管的实体,通常修改属性即可,是否额外调用 save 要与项目 API 约定和对象状态一致。
在 Boot 服务中组织工作单元
普通 Boot 应用由容器创建 Repository 和 EntityManager 代理。将一组必须原子完成的业务动作放进公共服务方法,并由事务代理调用:
@Transactional
public OrderView changeAmount(long id, long cents) {
PurchaseOrder order = repository.findById(id).orElseThrow();
order.setAmountCents(cents);
return new OrderView(order.getId(), order.getAmountCents());
}这里的返回类型是服务定义的 DTO。方法内还应加入实际业务需要的权限与金额校验,事务只组织数据库工作。
Spring 的事务型 EntityManager 随事务绑定。跨线程任务、事务传播和异常回滚条件应按 Spring 事务语义处理,不能把一个真实 EntityManager 塞进全局变量或传给并行任务共享。
Web 应用如果关闭 Open EntityManager in View,应在事务内完成需要的查询与 DTO 构造:
spring.jpa.open-in-view=falseOpen-in-view 延长 EntityManager 的可用范围,使视图或序列化阶段可能触发查询;它没有把整个 HTTP 请求自动变成一个事务。相关默认行为和配置见 Spring Boot SQL 数据访问。
乐观锁在写入时发现旧版本
运行:
run_lab optimistictwoContextsSameInitialVersion=true staleUpdateRejected=true两个 EntityManager 先读到相同 version。A 修改并提交后,数据库版本增加;B 用旧版本 flush,UPDATE 的版本条件匹配不到行,Hibernate 抛出 OptimisticLockException。
版本检查保护了当前映射实体的竞争更新。关联表、批量 JPQL、原生 SQL 和跨实体不变量,还需要各自的事务和约束设计。冲突后应回滚并关闭这次上下文,再重新读取当前状态,决定提示用户、合并修改还是重试业务动作。
重试不能继续使用失败 EntityManager 和它持有的旧对象。若一个操作还会发短信或调用外部系统,更要分清重试会重复哪些副作用。Hibernate 锁机制
ORM 故障从对象状态和实际 SQL 查起
先确定出错阶段
| 现象 | 先看什么 | 下一步 |
|---|---|---|
| setter 修改后数据库没变 | em.contains、事务是否活动、是否 detached | 在新事务加载受管对象,或使用 merge 返回值 |
| save 已返回,提交时报唯一约束 | flush/commit 阶段的原始 SQLState | 回滚,按唯一键冲突处理,勿只包住 save |
| 删除后新增同业务键仍冲突 | INSERT/DELETE 实际执行顺序 | 评估原位更新,或在明确需要时先 flush 删除 |
| bulk update 后对象仍旧 | 当前上下文是否已加载相同行 | 处理待同步变化后 clear/refresh 或重建上下文 |
| JSON 序列化触发懒加载异常 | DTO 构造位置与关联初始化 | 在事务内按返回需求抓取,避免全局 EAGER |
| UPDATE 影响零行并报版本冲突 | 对象 version 与数据库当前版本 | 回滚后重新读取,进行业务冲突决策 |
| 批量写入内存增长 | 上下文中的实体数量和集合引用 | 分段 flush/clear,同时评估事务持续时间 |
数据库错误通常被 ORM 和 Spring 再包装。保留完整 cause 与 SQLException 的 SQLState,尤其区分约束冲突、连接中断、锁等待和版本冲突;不要仅按最外层异常名称统一重试。
Hibernate 二级缓存跨上下文保存实体数据,查询缓存保存查询相关结果信息;两者与当前 EntityManager 的身份映射不同。本工程关闭二级和查询缓存,使脏检查、加载和独立 JDBC 验证不被缓存隐藏。启用缓存后,要重新核对外部写入失效和并发策略。
检查数据和事务是否释放
查看初始数据是否恢复:
docker compose -p da10-jpa exec -T db \
psql -X -U lab_owner -d jpa_lab -v ON_ERROR_STOP=1 \
-c "SELECT id, version, external_key, amount_cents FROM purchase_order ORDER BY id; SELECT count(*) FROM order_line;"应有两条种子订单,金额分别 500、100,版本为 0,明细一行。每个模式独立重置数据,序列可能继续增长;序列有间隙是正常现象,不能按连续主键判断是否发生回滚。
完整运行:
run_lab allordering 和 optimistic 是预期失败分支,Hibernate 可能输出约束错误或批次释放日志。只要相应布尔断言通过且进程退出码为 0,说明实验已识别失败并完成恢复;其他异常会让程序非零退出。
关闭环境
docker compose -p da10-jpa down
unset LAB_DB_PASSWORD LAB_APP_PASSWORD LAB_DB_URL LAB_DIR LAB_CACHE
unset -f run_lab仅移除这套实验容器和网络,tmpfs 数据随之消失。源码和下载缓存保留。生产服务应通过连接池、迁移工具和事务管理器管理各自资源,不能沿用实验重置表的逻辑。
权威资料与规范地址
实体状态与操作契约可查 Jakarta 规范,Hibernate 手册解释 provider 的 SQL 和抓取行为,Spring 文档说明 Repository 与事务集成。
| 资料 | 完整地址 |
|---|---|
| Jakarta Persistence 3.2 | https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2 |
| Spring JPA 集成 | https://docs.spring.io/spring-framework/reference/data-access/orm/jpa.html |
| Boot 依赖版本 | https://docs.spring.io/spring-boot/appendix/dependency-versions/coordinates.html |
| Boot 系统要求 | https://docs.spring.io/spring-boot/system-requirements.html |
| Hibernate 标识符 | https://docs.hibernate.org/orm/7.4/userguide/html_single/#identifiers |
| Hibernate 持久化上下文 | https://docs.hibernate.org/orm/7.4/userguide/html_single/#pc |
| Hibernate Flushing | https://docs.hibernate.org/orm/7.4/userguide/html_single/#flushing |
| Hibernate Associations | https://docs.hibernate.org/orm/7.4/userguide/html_single/#associations |
| Hibernate Fetching | https://docs.hibernate.org/orm/7.4/userguide/html_single/#fetching |
| Hibernate 继承映射 | https://docs.hibernate.org/orm/7.4/userguide/html_single/#entity-inheritance |
| Spring Data 实体持久化 | https://docs.spring.io/spring-data/jpa/reference/jpa/entity-persistence.html |
| Boot SQL 数据访问 | https://docs.spring.io/spring-boot/reference/data/sql.html |
| Hibernate 锁机制 | https://docs.hibernate.org/orm/7.4/userguide/html_single/#locking |
