Java高级篇之Hibernate:让对象映射到表,理解持久化与事务

上一篇用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程序运行过程的整体结构。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

相关推荐
学页技术极客1 小时前
出海教培系统选型调研:6 条技术路线的国际化能力比对
1024程序员节
CQU_JIAKE1 小时前
10.24[A]
1024程序员节
李子红了时1 小时前
《蓝星帝丘 · 百度网盘上传加速》使用说明(附用户体验)
1024程序员节
无忧.芙桃1 小时前
C++语言原理与实践(十八):set和map的使用
1024程序员节
Random Glow1 小时前
聚簇索引 vs 二级索引:回表、覆盖索引、索引下推
数据库·mysql·性能优化·innodb·1024程序员节·索引优化
Younglina1 小时前
三个晚上,我和 WorkBuddy 做了个生图网站
前端·后端·ai编程
怪我冷i1 小时前
Rust语言——关联类型
1024程序员节
深圳老胡1 小时前
STM32 开发中 Trae 内置模型怎么选:Seed、MiniMax、GLM、DeepSeek、Kimi、Qwen 全对比
笔记·stm32·单片机·嵌入式硬件·代码规范·1024程序员节
喵了几个咪1 小时前
GoWind Admin 拆解:一个 Sender 接口接 39 个通知渠道
微服务·golang·go·1024程序员节·admin·gowind