上一篇用JDBC完成了学生表的增删改查。每次查询都需要读取ResultSet、创建对象,再把列值逐个放进字段;修改对象后,还要另外写一条UPDATE。这些工作重复出现,正是ORM(Object-Relational Mapping,对象关系映射)要处理的部分。
这一篇继续使用学生信息,换成Hibernate完成同一组操作。重点放在一个实际问题上:为什么有时只调用setter就更新了数据库,有时修改对象却什么也没发生?答案藏在对象是否仍受持久化上下文管理,以及事务有没有提交。
一、先把Hibernate放回数据库访问链路
应用代码操作Student对象,Hibernate根据映射生成SQL,JDBC驱动将SQL交给数据库。数据库仍然负责约束、索引、并发和事务;ORM减少对象与数据行之间的重复转换。
Jakarta Persistence定义实体映射及持久化API,Hibernate是它的实现,同时提供Session等原生API。本文使用Hibernate原生启动方式,实体注解来自jakarta.persistence。两套名称对应同一组核心职责:SessionFactory对应EntityManagerFactory,Session对应EntityManager。
SessionFactory集中保存映射元数据并创建Session,通常由应用共享。Session承载一次工作单元中的对象状态,不能让多个线程同时使用。一次请求可以包含多个数据库操作,但事务边界要由业务决定。
本文示例固定使用Java21、Hibernate ORM7.4.13.Final和H2 2.5.252,便于直接复现。Hibernate版本与兼容性
二、用实体说明对象和表怎样对应
项目结构如下:
text
hibernate-demo/
├── pom.xml
└── src/main/java/demo/
├── Student.java
└── Main.java
pom.xml完整内容:
xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId><artifactId>hibernate-demo</artifactId><version>1.0</version>
<properties><maven.compiler.release>21</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>org.hibernate.orm</groupId><artifactId>hibernate-core</artifactId><version>7.4.13.Final</version></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>2.5.252</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version></plugin>
<plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.1.0</version></plugin>
</plugins></build>
</project>
Student.java:
java
package demo;
import jakarta.persistence.*;
@Entity
@Table(name = "students")
public class Student {
@Id
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Version
private long version;
protected Student() {}
public Student(Long id, String name) {
this.id = id;
this.name = name;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public long getVersion() { return version; }
}
这几个注解分别回答不同的问题:
| 映射 | 作用 | 本例结果 |
|---|---|---|
| @Entity | 声明实体类型 | HQL中使用Student |
| @Table | 指定数据库表 | students |
| @Id | 标识实体主键 | id作为唯一身份 |
| @Column | 指定列约束 | name非空,长度50 |
| @Version | 保存乐观锁版本 | 更新时检测并发修改 |
本例把注解放在字段上,采用字段访问,因此Hibernate可以直接读取私有字段。protected无参构造器供框架创建实体;业务构造器负责正常创建学生对象。
id由代码明确赋值,便于观察每一步。需要数据库生成主键时,再选择与数据库匹配的生成策略。不要把"Java字段有值"误当作"数据库已经写入":new Student只产生普通对象。
三、持久化上下文到底管理什么
把Session理解成一次数据库工作中的对象登记簿。同一个Session内,同一实体类型与主键对应同一个受管理对象。第二次find取到的是同一个Java实例,这既避免重复身份,也让后续修改集中到一份对象状态上。
对象生命周期可以分成四种状态:
| 状态 | 如何进入 | 修改字段之后 |
|---|---|---|
| 新建 | new Student | 只改变内存 |
| 托管 | persist或在Session中查询得到 | flush时检查并同步修改 |
| 游离 | Session关闭或对象被detach | 不再自动跟踪 |
| 删除 | 对托管对象调用remove | 安排数据库删除 |
脏检查发生在受管理对象与数据库状态同步时。托管对象的name变化会形成UPDATE,因此不需要额外调用"保存"方法。Session关闭以后,同一个对象变成游离对象,再改name不会自动写回数据库。

这也解释了一个常见错误:从服务层返回实体,在另一处修改它,然后期待Hibernate自动保存。先确认它是否仍处于当前持久化上下文,才能判断修改会不会被跟踪。持久化上下文与状态操作
四、完整跑通创建、更新、查询和删除
Main.java如下,数据库在内存中创建,不依赖外部MySQL服务:
java
package demo;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import java.util.function.Function;
public class Main {
static <T> T inTx(SessionFactory factory, Function<Session, T> work) {
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
try {
T result = work.apply(session);
tx.commit();
return result;
} catch (RuntimeException | Error failure) {
try {
if (tx.isActive()) tx.rollback();
} catch (RuntimeException rollbackFailure) {
failure.addSuppressed(rollbackFailure);
}
throw failure;
}
}
}
public static void main(String[] args) {
Configuration cfg = new Configuration()
.addAnnotatedClass(Student.class)
.setProperty("hibernate.connection.driver_class", "org.h2.Driver")
.setProperty("hibernate.connection.url", "jdbc:h2:mem:school;DB_CLOSE_DELAY=-1")
.setProperty("hibernate.connection.username", "sa")
.setProperty("hibernate.connection.password", "")
.setProperty("hibernate.hbm2ddl.auto", "create-drop")
.setProperty("hibernate.show_sql", "true")
.setProperty("hibernate.format_sql", "true");
try (SessionFactory factory = cfg.buildSessionFactory()) {
inTx(factory, s -> { s.persist(new Student(1L, "Lin")); return null; });
inTx(factory, s -> {
Student first = s.find(Student.class, 1L);
Student second = s.find(Student.class, 1L);
System.out.println("same instance = " + (first == second));
first.setName("Lin Updated");
return null;
});
Student detached = inTx(factory, s -> s.find(Student.class, 1L));
System.out.println("after commit = " + detached.getName());
System.out.println("version = " + detached.getVersion());
detached.setName("Only In Memory");
String stored = inTx(factory, s -> s.find(Student.class, 1L).getName());
System.out.println("after detached edit = " + stored);
try {
inTx(factory, s -> {
s.find(Student.class, 1L).setName("Rolled Back");
s.flush();
throw new IllegalStateException("demo rollback");
});
} catch (IllegalStateException expected) {
System.out.println(expected.getMessage());
}
String afterRollback = inTx(factory, s -> s.find(Student.class, 1L).getName());
System.out.println("after rollback = " + afterRollback);
long count = inTx(factory, s -> s.createSelectionQuery(
"select count(s) from Student s where s.name = :name", Long.class)
.setParameter("name", "Lin Updated").getSingleResult());
System.out.println("matched = " + count);
inTx(factory, s -> { s.remove(s.find(Student.class, 1L)); return null; });
long remaining = inTx(factory, s -> s.createSelectionQuery(
"select count(s) from Student s", Long.class).getSingleResult());
System.out.println("remaining = " + remaining);
}
}
}
在项目目录执行:
bash
mvn -q compile exec:java -Dexec.mainClass=demo.Main
本例打开SQL日志,可以把业务输出和生成的INSERT、SELECT、UPDATE、DELETE放在一起读。先关注以下业务结果:
text
same instance = true
after commit = Lin Updated
version = 1
after detached edit = Lin Updated
demo rollback
after rollback = Lin Updated
matched = 1
remaining = 0
第一段persist把新对象纳入Session,事务提交后学生记录保存。第二段连续find两次,打印true,随后只调用setName;事务提交前的同步会生成UPDATE。
第三段把实体返回到Session外,修改为Only In Memory。新的Session重新查询,数据库仍是Lin Updated。这里特意使用新Session读取,避免把内存对象的值误当成数据库结果。
最后用HQL查询、remove删除,再统计剩余记录。HQL使用实体名与属性名 ,所以写Student和s.name;SQL才使用students表名。参数通过setParameter绑定,不把输入拼接到查询文本中。Hibernate入门示例
五、flush和commit之间差在哪里
示例故意在修改名称后调用flush,再抛出异常。此时UPDATE已经发送给数据库,但事务尚未提交,rollback仍会撤销它。新的Session读到Lin Updated,说明刷新和提交是两个步骤。
flush负责同步,commit负责提交事务。 默认刷新模式下,提交前会刷新,某些查询前也可能为了保持查询结果一致而刷新。业务代码不应该依赖"SQL一定拖到最后才执行"。
inTx把提交与回滚放在同一个结构里:业务操作成功就提交,发生运行时异常或Error就尝试回滚,然后重新抛出原异常。回滚自身失败时将其作为suppressed异常保留。无论哪条路径,try-with-resources都会关闭Session。
一次转账、一次订单确认等需要原子完成的操作,应放进同一事务,而不是每个方法各自提交。本文每次inTx是一个独立事务,是为了清晰展示状态转换。不要把这个演示辅助方法机械地拆到每一次DAO调用上。
六、从入门走向实际项目,要盯住这几个地方
第一,建表策略只用于演示。 create-drop会创建表并在SessionFactory关闭时删除,不能放进保存正式数据的环境。生产项目使用明确的数据库迁移脚本管理结构,配合校验确认实体映射与数据库一致。
第二,Session内置的对象复用不是跨请求缓存。 新Session不会自动继承旧Session的实体实例。查询性能要看SQL次数、返回行数和索引,不能因为使用ORM就假设数据库访问很少。
第三,关联加载要按使用场景设计。 查询订单列表后逐条访问明细,可能触发N+1查询;在需要关联数据的查询处安排fetch join、实体图或DTO投影,并检查实际SQL。把所有关联改成急加载会扩大每次查询,不是通用解法。
第四,乐观锁检测冲突,不替业务决定合并。 @Version使更新带上版本条件。另一事务已经改过同一行时,旧版本更新会失败;应用需要重新读取并决定重试、提示冲突或执行合并,不能覆盖掉其他人的修改。
第五,ORM与SQL各有合适位置。 实体生命周期、关系维护和常规业务写入适合ORM;复杂报表、聚合与专项调优仍应直接审视SQL。掌握JDBC、事务和数据库约束,才能读懂ORM真正执行了什么。
七、🧠 思维导图
#mermaid-svg-WVPbnVznNfrxJolS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WVPbnVznNfrxJolS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WVPbnVznNfrxJolS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WVPbnVznNfrxJolS .error-icon{fill:#552222;}#mermaid-svg-WVPbnVznNfrxJolS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WVPbnVznNfrxJolS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WVPbnVznNfrxJolS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WVPbnVznNfrxJolS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WVPbnVznNfrxJolS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WVPbnVznNfrxJolS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WVPbnVznNfrxJolS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WVPbnVznNfrxJolS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WVPbnVznNfrxJolS .marker.cross{stroke:#333333;}#mermaid-svg-WVPbnVznNfrxJolS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WVPbnVznNfrxJolS p{margin:0;}#mermaid-svg-WVPbnVznNfrxJolS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WVPbnVznNfrxJolS .cluster-label text{fill:#333;}#mermaid-svg-WVPbnVznNfrxJolS .cluster-label span{color:#333;}#mermaid-svg-WVPbnVznNfrxJolS .cluster-label span p{background-color:transparent;}#mermaid-svg-WVPbnVznNfrxJolS .label text,#mermaid-svg-WVPbnVznNfrxJolS span{fill:#333;color:#333;}#mermaid-svg-WVPbnVznNfrxJolS .node rect,#mermaid-svg-WVPbnVznNfrxJolS .node circle,#mermaid-svg-WVPbnVznNfrxJolS .node ellipse,#mermaid-svg-WVPbnVznNfrxJolS .node polygon,#mermaid-svg-WVPbnVznNfrxJolS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WVPbnVznNfrxJolS .rough-node .label text,#mermaid-svg-WVPbnVznNfrxJolS .node .label text,#mermaid-svg-WVPbnVznNfrxJolS .image-shape .label,#mermaid-svg-WVPbnVznNfrxJolS .icon-shape .label{text-anchor:middle;}#mermaid-svg-WVPbnVznNfrxJolS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WVPbnVznNfrxJolS .rough-node .label,#mermaid-svg-WVPbnVznNfrxJolS .node .label,#mermaid-svg-WVPbnVznNfrxJolS .image-shape .label,#mermaid-svg-WVPbnVznNfrxJolS .icon-shape .label{text-align:center;}#mermaid-svg-WVPbnVznNfrxJolS .node.clickable{cursor:pointer;}#mermaid-svg-WVPbnVznNfrxJolS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WVPbnVznNfrxJolS .arrowheadPath{fill:#333333;}#mermaid-svg-WVPbnVznNfrxJolS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WVPbnVznNfrxJolS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WVPbnVznNfrxJolS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WVPbnVznNfrxJolS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WVPbnVznNfrxJolS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WVPbnVznNfrxJolS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WVPbnVznNfrxJolS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WVPbnVznNfrxJolS .cluster text{fill:#333;}#mermaid-svg-WVPbnVznNfrxJolS .cluster span{color:#333;}#mermaid-svg-WVPbnVznNfrxJolS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WVPbnVznNfrxJolS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WVPbnVznNfrxJolS rect.text{fill:none;stroke-width:0;}#mermaid-svg-WVPbnVznNfrxJolS .icon-shape,#mermaid-svg-WVPbnVznNfrxJolS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WVPbnVznNfrxJolS .icon-shape p,#mermaid-svg-WVPbnVznNfrxJolS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WVPbnVznNfrxJolS .icon-shape .label rect,#mermaid-svg-WVPbnVznNfrxJolS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WVPbnVznNfrxJolS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WVPbnVznNfrxJolS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WVPbnVznNfrxJolS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Hibernate ORM
对象映射
Entity与Table
Id与Column
持久化上下文
同一主键同一实例
托管与游离
数据库操作
persist与find
脏检查与remove
事务
flush发送SQL
commit或rollback
查询与工程选择
HQL参数绑定
版本锁与加载计划
八、总结
总结要点
映射决定对象怎样落到表上。 Entity、Id和Column将类型、身份与列对应起来,Hibernate再通过JDBC执行数据库操作。
持久化上下文决定修改是否被跟踪。 托管对象可以依靠脏检查同步;游离对象的字段变化只发生在内存中。判断状态,比寻找一个通用"保存"方法更重要。
事务决定哪些结果最终保留。 flush发送SQL,commit提交,rollback撤销当前事务的修改。运行示例时同时观察SQL与重新查询的结果,就能把这三个动作区分清楚。
数据库编程这一部分到这里完成。下一篇进入JVM,先梳理类加载、运行时数据区与执行引擎,建立Java程序运行过程的整体结构。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊