Hibernate6-入门手册-全-
Hibernate6 入门手册(全)
一、Hibernate 6 简介
大多数重要的开发项目都涉及到关系数据库。 1
随着万维网的出现,对数据库的需求增加了。尽管他们可能不知道,网上书店和报纸的顾客正在使用数据库。在应用内部的某个地方,正在查询一个数据库并给出响应。
Hibernate 是一个库(实际上是一组库),它通过将关系数据呈现为简单的 Java 对象来简化 Java 应用中关系数据库的使用,并通过会话管理器进行访问,因此被称为“对象/关系映射器”,或 ORM。它提供了两种编程接口:“本地 Hibernate”接口和 Jakarta EE 2 标准 Java 持久化 API。
这个版本主要关注 Hibernate 6。在写这句话的时候,它还在 Alpha8 上,所以还没有正式发布,但是实际发布的版本很可能与这里使用的代码非常相似。
有些解决方案适合 ORM——比如 Hibernate——有些适合通过 Java 数据库连接(JDBC) API 直接访问的传统方法。我们认为 Hibernate 是一个很好的首选,因为它不排除同时使用其他方法,尽管如果数据是从两个不同的 API 修改的,就必须小心了。
为了说明 Hibernate 的一些优势,本章我们来看一个使用 Hibernate 的简单例子,并将其与传统的 JDBC 方法进行对比。
普通旧 Java 对象(POJOs)
作为一种面向对象的语言,Java 处理对象。通常,表示程序状态的对象相当简单,包含属性(或特性)和改变或检索这些属性的方法(赋值函数和访问函数,通俗地说就是“setters”和“getters”)。一般来说,这些对象可能封装了一些关于属性的行为,但通常它们的唯一目的是包含一个程序状态。这些通常被称为“普通旧 Java 对象”,或 POJOs。
在理想的情况下,获取任何 Java 对象——不管是不是普通的——并将其持久化到数据库中是很简单的事情。实现这一点不需要特殊的编码,也不会影响性能,而且结果是完全可移植的。在这个理想的世界中,我们也许会以这样的方式执行这样的操作。
POJO pojo=new POJO();
ORMSolution magic=ORMSolution.getInstance();
magic.save(pojo);
Listing 1-1.A Rose-Tinted View of Object Persistence
不会有令人讨厌的意外,不会有额外的工作将类与表模式关联起来,也不会有性能问题。
Hibernate 实际上非常接近这个想法,至少与它的许多替代品相比是如此, 3 但是需要创建配置文件,还要考虑微妙的性能和同步问题。然而,Hibernate 确实实现了它的基本目标:它允许您在数据库中简单地存储 POJOs。图 1-1 展示了 Hibernate 如何在客户端代码和数据库之间融入你的应用。

图 1-1
Hibernate 在 Java 应用中的作用
构建项目
我们将使用 Maven ( https://maven.apache.org )为这本书构建一个项目。它将被组织成一个顶级项目,每个章节都有一个子项目(或“模块”),我们还会有一些额外的模块来提供通用功能。你可以对 Gradle ( https://gradle.org )做同样的事情,这本书没有真正的理由偏爱其中一个,但 Maven 赢得了硬币,Maven 就是这样。
在您的文件系统上创建一个目录;可以是你喜欢的任何东西。(在我的系统上,它是/Users/joeo/work/publishing/bh6/src,但是您可以根据自己的喜好给它起任何合适的名字,当然还有文件系统。)这将是我们的顶级目录;我们将按名称把章节放在里面,像chapter01之类的。我们使用两位数,因为它看起来更好,也可以正确排序。 5
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns:="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>hibernate-6-parent</artifactId>
<packaging>pom</packaging>
<version>5.0</version>
<modules>
<module>chapter01</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<testng.version>7.4.0</testng.version>
<hibernate.core.version>6.0.0.Alpha8</hibernate.core.version>
<h2.version>1.4.200</h2.version>
<logback.version>1.2.3</logback.version>
<lombok.version>1.18.18</lombok.version>
<hibernate.validator.version>
6.2.0.Final
</hibernate.validator.version>
<javax.el-api.version>3.0.0</javax.el-api.version>
<ignite.version>2.10.0</ignite.version>
<jackson.version>2.12.3</jackson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.core.version}</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-core</artifactId>
<version>${ignite.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
<version>${hibernate.core.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate.core.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
Listing 1-2The Top-Level pom.xml
那么这个pom.xml到底在做什么呢?事实证明,相当多——尽管几乎所有的内容都与本书其余部分的通用配置相关,所以模块pom.xml文件要比其他情况简单得多。
前几行描述了“父项目”,被描述为具有com.autumncode.books.hibernate的groupId和hibernate-6-parent的工件 id。它也被设定为1.0-SNAPSHOT的版本——这些都不是特别相关。
然后我们有一个<modules>块,其中有一个模块。随着我们阅读本书的进展,我们将在这里为每一章添加模块,如果您查看本书附带的源代码,您将在本节中看到模块的完整补充。
接下来是<properties>块,我们用它来设置默认的编译器版本和目标(Java 11,这是 Java 6 的当前“生产”版本),后面是很多特定的依赖版本,比如<h2.version>1.4.200</h2.version>。 7
接下来,我们有一个<dependencyManagement>块。这实际上并没有设置任何依赖关系:它只是允许我们集中引用依赖关系。请注意,模块将继承父项目的依赖项,因此我们可以在这里声明所有特定的依赖项,模块可以简单地使用名称,而不必包含版本。例如,如果 Hibernate 的一个新版本出来了,我们只需要改变在<dependencyManagement>中使用的版本,这个改变将会在整个项目中传播。
在<dependencyManagement>之后,我们有了我们期望在整个项目中通用的依赖关系。这个是一个 Hibernate 本,所以这里有 Hibernate 本身是有意义的,还有一个相对标准的日志框架(Logback ( https://logback.qos.ch/ ),它本身包括 Slf4j ( www.slf4j.org/ )作为传递依赖),我们还导入 TestNG ( https://testng.org )和 H2 ( www.h2database.com/ ),一个用纯 Java 编写的流行的嵌入式数据库,作为测试依赖。
最后,我们有一个<build>部分,它强制 Maven 使用最近版本的maven-compiler-plugin,这是正确设置语言版本所必需的,因为虽然 Maven 非常有用,但它也非常好地支持遗留 JVM,以至于您必须明确告诉它使用更新的 JVM。
我们甚至还没有看到这一章的构建!我们声明了模块,但是还没有描述它。令人欣慰的是,有了这么多在父pom.xml中完成的工作,这一章的项目模型真的相当简单。
<?xml version="1.0" encoding="UTF-8"?>
<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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>hibernate-6-parent</artifactId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter01</artifactId>
</project>
Listing 1-3chapter01/pom.xml
在这里,我们所做的就是声明这个模块是什么(chapter01)并包含一个对父模块的引用。一切都是遗传的。
既然我们已经解决了所有棘手的项目问题,让我们回到 Hibernate 的设计初衷上来。
正如已经指出的,Hibernate 是一个“对象/关系映射器”,这意味着它将 Java 对象映射到关系模式,反之亦然。程序员实际上对实际映射的样子有很大的控制权,但是一般来说,遵循一些简单的习惯用法来创建容易映射的对象是最容易的。让我们从一个简单的对象开始,它代表一条消息,我们将它存储在数据库中,除了作为一个简单示例的基础之外,没有任何好的理由。
package chapter01.pojo;
import java.util.Objects;
public class Message {
String text;
public Message(String text) {
setText(text);
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Message)) return false;
Message message = (Message) o;
return Objects.equals(getText(), message.getText());
}
@Override
public int hashCode() {
return Objects.hash(getText());
}
@Override
public String toString() {
return String.format("Message{text='%s'}", getText());
}
}
Listing 1-4chapter01/src/main/java/chapter01/pojo/Message.java
你不可能得到比那个对象更简单的东西;当然,这是可行的,因为您可以创建一个没有状态的对象(因此,没有text字段,也没有访问器或赋值器来引用它),我们也可以忽略equals()、hashCode()和toString()。这样的对象作为 actors ,作用于其他对象的对象,会很有用。但是清单 1-4 是大多数POJO 的一个很好的例子,因为大多数表示程序状态的 Java 类也有属性、访问器、变异器、equals、hashCode和toString。**
**Hibernate 可以很容易地映射清单 1-4 ,但是它没有而不是遵循大多数 Hibernate 实体中的习惯用法。不过,到达那里真的很简单。
让我们创建一个MessageEntity类,它仍然不完全符合 Hibernate 的习惯用法,但是已经为持久化做好了准备——在这个过程中,它将作为一个实际的 Hibernate 实体的基础,在我们看到 Hibernate 在幕后为我们做了什么之后,我们将马上看到它。
package chapter01.pojo;
import java.util.Objects;
public class MessageEntity {
Long id;
String text;
public MessageEntity() {
}
public MessageEntity(Long id, String text) {
this();
setId(id);
setText(text);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MessageEntity)) return false;
MessageEntity message = (MessageEntity) o;
return Objects.equals(getId(), message.getId())
&& Objects.equals(getText(), message.getText());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getText());
}
@Override
public String toString() {
return String.format("MessageEntity{id=%d,text='%s'}",
getId(),
getText());
}
}
Listing 1-5chapter01/src/main/java/chapter01/pojo/MessageEntity.java
这里有一些变化。我们已经为id添加了一个id字段(一个Long),以及一个访问器和一个赋值器,我们已经为标准的实用程序方法(equals()、hashCode()和toString())添加了id…我们还添加了一个无参数的构造函数。对于关系映射来说,id字段非常常见,因为在处理数据库时,这样的字段更容易搜索和引用,但是无参数构造函数主要是为了方便起见,因为它允许我们创建一种“空白画布”对象,如果我们允许的话,我们可以稍后通过赋值函数或直接字段访问来填充它。
用严格的 OOP 术语来说,这可能是一件坏事,因为这意味着我们可以合法地构造一个缺少合法状态的对象;想想我们可怜的老朋友。如果我们将“有效的MessageEntity”定义为具有一个有效的id字段(任何数字都可以,只要不是null)和一个填充的text字段(除了null),那么调用我们的无参数构造函数会创建一个而不是有效的MessageEntity。事实上,如果我们调用 other 构造函数,我们会有类似的问题,因为我们在设置属性值时没有检查它们。
这实际上是 Java 持久化 API 或 JPA 规范的一个特征,它说类必须有一个没有参数的public或protected无参数构造函数。Hibernate 扩展了 JPA 规范,虽然它在某些要求上比 JPA 规范宽松,但它通常遵循构造函数的要求(尽管构造函数也可以有package可见性)。
我们也不应该将类标记为final。实际上有一些方法可以解决这个问题,但是 Hibernate 默认创建了一个类的扩展来实现一些潜在的非常有用的特性(比如在属性中延迟加载数据)。
你还应该提供标准的访问器和赋值器(比如getId()和setId())。
那么我们如何在实际的持久化故事中使用这个类呢?这里有一个测试类,它实际上初始化了一个数据库,将一个MessageEntity保存到其中,然后测试消息是否可以被正确检索。
package chapter01.jdbc;
import chapter01.pojo.MessageEntity;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class PersistenceTest {
Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:h2:./db1", "sa", "");
}
@BeforeClass
public void setup() {
final String DROP = "DROP TABLE messages IF EXISTS";
final String CREATE = "CREATE TABLE messages ("
+ "id BIGINT GENERATED BY DEFAULT AS IDENTITY "
+ "PRIMARY KEY, "
+ "text VARCHAR(256) NOT NULL)";
try (Connection connection = getConnection()) {
// clear out the old data, if any, so we know the state of the DB
try (PreparedStatement ps =
connection.prepareStatement(DROP)) {
ps.execute();
}
// create the table...
try (PreparedStatement ps =
connection.prepareStatement(CREATE)) {
ps.execute();
}
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public MessageEntity saveMessage(String text) {
final String INSERT = "INSERT INTO messages(text) VALUES (?)";
MessageEntity message = null;
try (Connection connection = getConnection()) {
try (PreparedStatement ps =
connection.prepareStatement(INSERT,
Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, text);
ps.execute();
try (ResultSet keys = ps.getGeneratedKeys()) {
if (!keys.next()) {
throw new SQLException("No generated keys");
}
message = new MessageEntity(keys.getLong(1), text);
}
}
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return message;
}
@Test
public void readMessage() {
final String text = "Hello, World!";
MessageEntity message = saveMessage(text);
final String SELECT = "SELECT id, text FROM messages";
List<MessageEntity> list = new ArrayList<>();
try (Connection connection = getConnection()) {
try (PreparedStatement ps =
connection.prepareStatement(SELECT)) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
MessageEntity newMessage = new MessageEntity();
newMessage.setId(rs.getLong(1));
newMessage.setText(rs.getString(2));
list.add(message);
}
}
}
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
assertEquals(list.size(), 1);
for (MessageEntity m : list) {
System.out.println(m);
}
assertEquals(list.get(0), message);
}
}
Listing 1-6chapter01/src/test/java/chapter01/jdbc/PersistenceTest.java
这是怎么回事?首先,我们有一个简单的实用方法,它返回一个Connection;这在很大程度上节省了语句的长度,减少了重复。
我们还有一个setup()方法,标有@BeforeClass。这个注释意味着这个方法将在类中的任何测试执行之前被调用。(我们也可以使用@BeforeTest或@BeforeSuite,但是在这种情况下,@BeforeClass可能是合适的粒度,假设我们有比实际更多的功能要测试。)
注释指示了被注释的方法在测试类的上下文中何时运行。@BeforeTest在每个用@Test标注的方法运行之前运行。@BeforeClass在类中的任何测试方法运行之前运行;@BeforeSuite在任何测试类运行之前运行。还有@AfterClass、@AfterTest和@AfterSuite方法,它们在相应的阶段结束时运行。
接下来,我们有另一个实用方法saveMessage(),它接受消息文本进行保存。这将在数据库表中插入一条新记录。它从数据库中请求生成的密钥,这样它可以填充一个MessageEntity并返回它,反映了方便的行为(我们现在可以查询消息的并测试等价性,就像我们在readMessage()测试中看到的那样)。它很实用。说实话,这不是很好,但也不值得改进。Hibernate 在这方面做得比我们好得多,而且代码更少;我们可以模仿 Hibernate 的大部分功能,但是这是不值得的。
最后,我们进行实际的测试:readMessage()。这将调用saveMessage(),然后通读所有“保存的消息”——考虑到我们已经尽力创建了一个确定性的数据库状态,它将是一个由和消息组成的列表。当它读取消息时,它为每个消息创建MessageEntity对象,并将它们存储在一个List中,然后我们验证List——它应该只有一个元素,并且该元素应该与我们在方法开始时保存的MessageEntity相匹配。
咻!那是很大的工作量;在资源的获取中有一些样板文件(通过自动资源管理完成,在异常情况下处理干净的释放),JDBC 代码本身是相当低级的。它也相当动力不足,非常手动。我们仍然在管理特定的资源,比如Connection和PreparedStatement,代码非常脆弱;如果我们添加了一个字段,我们就必须查找并修改受该字段影响的每一条语句,因为我们是手动将数据从 JDBC 映射到我们的对象中。 8
在这段代码中,我们还会遇到类型的问题。毕竟,这是一个非常简单的对象;它存储一个简单的数字标识符和一个简单的字符串。然而,如果我们想要存储地理位置,我们必须将地理位置分解成它的组件属性(例如,纬度和经度)并分别存储,这意味着您的对象模型不再完全匹配您的数据库。
所有这些使得直接使用数据库看起来越来越有缺陷,这还没有考虑到围绕对象持久化和检索的其他问题。
想让运行这些测试吗?这真的很简单:用mvn build运行 Maven 生命周期,它将下载我们项目的所有依赖项(如果需要的话),编译我们的“生产”类(src/main/java中的那些),然后编译我们的测试类(src/test/java中的那些),然后执行测试,转储任何控制台输出(自然是到控制台)并在失败时暂停。然后它构建了一个我们生产资源的罐子。我们还可以将生命周期限制为仅仅用mvn test运行测试。
将 Hibernate 作为持久化解决方案
Hibernate 修复了几乎所有我们不喜欢 JDBC 解决方案的地方。在这个例子中我们不使用复杂类型,所以我们直到本书的后面才会看到这是如何实现的,但是在几乎所有的度量中这样做都更容易。 9
首先,我们需要将我们的MessageEntity修改成一个真正的 Hibernate 实体。我们通过向类添加一些注释来做到这一点,使它符合 JPA 的要求。我们还将稍微改变构造函数,以更好地适应Message的域;一个Message的核心属性是它的text,而id是附带的。我们可以用 Hibernate 比用 ?? 更好地映射 ??。 10 我们想为 JPA 添加四个注释,它们实际上涵盖了 Hibernate 用户最常使用的注释: 11
-
@javax.persistence.Entity:将类标记为由 Hibernate 管理的实体类 -
@javax.persistence.Id:使其应用的字段成为数据库的主键 -
@javax.persistence.GeneratedValue:向 Hibernate 提供应该如何填充值的信息 -
@javax.persistence.Column:允许我们控制数据库中字段的各个方面
这里是Message实体本身。
package chapter01.hibernate;
import javax.persistence.*;
import java.util.Objects;
@Entity
public class Message {
Long id;
String text;
public Message() {
}
public Message(String text) {
this();
setText(text);
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column(nullable = false)
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Message)) return false;
Message message = (Message) o;
return Objects.equals(getId(), message.getId())
&& Objects.equals(getText(), message.getText());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getText());
}
@Override
public String toString() {
return String.format("Message{id=%d,text='%s'}",
getId(),
getText());
}
}
Listing 1-7chapter01/src/main/java/chapter01/hibernate/Message.java
这里的@GeneratedValue有一个GenerationType.IDENTITY的strategy,它指定 Hibernate 将镜像我们手动创建的 JDBC 模式的行为:每个Message的键将由数据库自动生成。
@Column(nullable = false)同样表示text字段不能在数据库中存储null。列名将从字段名中派生出来,如果它匹配一个保留字,将会被稍微改动;在这种情况下,我们的数据库有一个名为text的列就可以了,所以不会发生混乱,如果我们需要的话,我们可以提供一个显式的列名。
除了注释和构造函数,Message和MessageEntity非常相似。
接下来,我们需要看看我们如何告诉 Hibernate 连接到数据库,以及它应该如何表现。我们通过一个配置文件来实现这一点,该文件通常被命名为hibernate.cfg.xml,位于执行类路径中;一般来说,除了 JDBC URL 和mapping引用,这些文件看起来都是一样的。因为这是为测试而写的,我们将把它放在我们的src/test/resources目录中。
<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:./db1</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create-drop</property>
<mapping class="chapter01.hibernate.Message"/>
</session-factory>
</hibernate-configuration>
Listing 1-8chapter01/src/test/resources/hibernate.cfg.xml
老实说,我们的大多数配置看起来都与此非常相似。但是它告诉我们什么呢?
| `connection` 1 `.driver.class` | 这是会话工厂的 JDBC 驱动程序类的完全限定名。 | | `connection.url` | 这是用于连接数据库的 JDBC URL。 | | `connection.username` | 令人惊讶的是,连接的用户名。 | | `connection.password` | 另一个惊喜是连接的密码。在未初始化的 H2 数据库中,“sa”和空密码就足够了。 | | `dialect` | 这个属性告诉 Hibernate 如何为特定的数据库编写 SQL。 | | `show_sql` | 该属性将 Hibernate 设置为将其生成的 SQL 语句回显到指定的记录器。 | | `hbm2ddl.auto` | 这个属性告诉 Hibernate 它是否应该管理数据库模式;在本例中,我们告诉它在初始化时创建,并在完成后删除数据库。 |hbm2ddl.auto在生产环境中是否危险。对于临时或测试环境,这没什么大不了的,但是当您谈论需要保存的真实数据时,这种属性可能是破坏性的,在谈论有价值的数据时,人们很少想听到这个词。
最后一行告诉 Hibernate 它有一个实体类型需要管理,即chapter01.hibernate.Message类。
还有一个配置文件需要考虑,尽管它是可选的。(包含在本书的资料中。)父项目将logback-classic指定为依赖项,这意味着每一章都接收 Logback 及其可传递的依赖项作为类路径元素。Logback 有一个默认的配置,但是对于我们的目的来说,它会非常嘈杂。这里有一个logback.xml配置文件,它去掉了一些噪声。
<configuration>
<appender
name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>
%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
</Pattern>
</encoder>
</appender>
<logger name="org.hibernate.SQL"
level="debug"
additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.hibernate.type.descriptor.sql"
level="trace"
additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
Listing 1-9chapter01/src/main/resources/logback.xml
请注意,默认记录器级别设置为info。这往往会在记录器的输出流(控制台)上创建大量信息;它看起来很有趣,对诊断很有帮助,但是如果你愿意,你可以将日志级别设置为error,大大减少 Hibernate 的麻烦。
现在我们已经完成了所有的准备工作和二级配置文件:终于是时候看看实际的 Hibernate 代码了。我们的测试实际上反映了 JDBC 测试所做的,几乎完全一样。这比 JDBC 代码简洁得多。
package chapter01.hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class PersistenceTest {
private SessionFactory factory = null;
@BeforeClass
public void setup() {
StandardServiceRegistry registry =
new StandardServiceRegistryBuilder()
.configure()
.build();
factory = new MetadataSources(registry)
.buildMetadata()
.buildSessionFactory();
}
public Message saveMessage(String text) {
Message message = new Message(text);
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
session.persist(message);
tx.commit();
}
return message;
}
@Test
public void readMessage() {
Message savedMessage = saveMessage("Hello, World");
List<Message> list;
try (Session session = factory.openSession()) {
list = session
.createQuery("from Message", Message.class)
.list();
}
assertEquals(list.size(), 1);
for (Message m : list) {
System.out.println(m);
}
assertEquals(list.get(0), savedMessage);
}
}
Listing 1-10chapter01/src/test/java/chapter01/hibernate/PersistenceTest.java
首先要注意的是我们获取资源的方式。在 JDBC 版本中,我们有一个简单的getConnection(),当我们碰巧需要一个Connection时,我们就调用它;这里,我们创建一个对SessionFactory的引用,并在类的测试运行之前初始化它。我们构建它的方式……并不复杂,但是对于我们可能会反复做的事情来说,它就显得冗长了。 12
一旦我们有了这个SessionFactory,这些习语就非常简单明了。我们创建一个Session在范围内的块——同样,使用自动资源管理——然后我们开始一个事务。(在 JDBC 的例子中,我们做了同样的事情,只是没有明说。)然后我们save()这个对象,或者根据需要查询一个。
一旦我们对数据库做了一些事情,我们就提交事务。
我们将在以后的章节中更多地讨论实际的配置和映射;如果您想知道有哪些设置可用,有哪些操作,以及为什么需要一个读操作的事务,这没关系。我们将涵盖所有这些内容。
如果您运行这段代码(同样使用mvn test或mvn build,您可能会看到一个吨的日志输出,这主要是因为在 Hibernate 配置文件中show_sql属性被设置为true。
摘要
在这一章中,我们已经考虑了驱动 Hibernate 发展的问题和需求。我们已经查看了一个简单示例应用的一些细节,这个应用是在 Hibernate 的帮助下编写的,也有不借助 Hibernate 编写的。我们已经掩饰了一些实现细节,但是我们将在第二章中深入讨论这些细节。
**二、集成和配置 Hibernate
将 Hibernate 集成到 Java 应用中很容易。Hibernate 的设计者避免了现有 Java 持久化解决方案中一些更常见的陷阱和问题,并创建了一个简洁而强大的架构。实际上,这意味着您不必在任何特定的 Java EE 容器或框架中运行 Hibernate。从 Hibernate 6 开始,由于集成了日期和时间 API 以及其他有用的特性,需要 Java 8 或更高版本。 1
你会用 Java 8,不代表你就应该用 Java 8。正如在第一章中提到的,写这篇文章的时候,Java 的最新版本是 Java 16,Java 17 即将发布;Java 的“长期”当前版本是 11,17 被指定为下一个长期支持版本。Java 8 应该只在你别无选择的情况下使用,比如不可避免地依赖于一个在 8 上受支持而在以后又不受支持的库。
起初,将 Hibernate 添加到您的 Java 项目看起来令人生畏:发行版包括一大组依赖项。要让您的第一个 Hibernate 应用工作,您必须设置数据库引用和 Hibernate 配置,这可能包括将您的对象映射到数据库。您还必须创建您的 POJOs,包括任何基于注释的映射。完成所有这些之后,您需要在您的应用中编写使用 Hibernate 来实际完成某件事情的逻辑!但是一旦你学会了如何将 Hibernate 集成到你的应用中,这些基础知识适用于任何使用 Hibernate 的项目。
Hibernate 设计的关键特性之一是最小侵入性原则:Hibernate 开发人员不希望 Hibernate 不必要地侵入您的应用。这导致了 Hibernate 的几个架构决策。在第一章中,您看到了如何使用传统的 Java 对象应用 Hibernate 来解决持久化问题。在本章中,我们将解释支持这种行为所需的一些配置细节。
集成和配置 Hibernate 所需的步骤
本章详细解释了配置和集成,但是为了快速浏览,请参考下面的列表,以确定您需要做些什么来启动并运行您的第一个 Hibernate 应用。然后第三章将带领你构建两个使用 Hibernate 的小示例应用。第一个例子非常简单,因此它很好地介绍了以下必要步骤:
-
识别具有数据库表示的 POJOs。
-
确定那些 POJOs 的哪些属性需要持久化。
-
注释每个 POJOs,将 Java 对象的属性映射到数据库表中的列(在第六章中有更详细的介绍)。
-
使用模式导出工具创建数据库模式,使用现有数据库,或者创建自己的数据库模式。
-
将 Hibernate Java 依赖项添加到您的应用的类路径中(将在本章中介绍)。
-
创建一个 Hibernate XML 配置文件,该文件指向您的数据库和映射的类(本章将介绍)。
-
在您的 Java 应用中,创建一个 Hibernate 配置对象,该对象引用您的 XML 配置文件(将在本章中介绍)。
-
同样在您的 Java 应用中,从配置对象构建一个 Hibernate SessionFactory 对象(将在本章中介绍)。
-
从 SessionFactory 中检索 Hibernate 会话对象,并为应用编写数据访问逻辑(创建、检索/读取、更新和删除)。
如果您不理解列表中提到的每个术语或概念,也不要担心。看这个列表,它实际上比你想象的要简单得多!读完这一章,然后按照下一章的例子,你就会知道这些术语的意思以及它们是如何组合在一起的。
理解 Hibernate 在 Java 应用中的位置
您可以直接从 Java 应用调用 Hibernate,也可以通过另一个框架访问 Hibernate,比如 Spring Data ( https://spring.io/projects/spring-data )。您可以从 Swing 应用、servlet、portlet、JSP 页面或任何其他可以访问数据库的 Java 应用中调用 Hibernate。 2 通常,你会使用 Hibernate 为应用创建一个数据访问层,或者替换现有的数据访问层。
Hibernate 支持 Java 管理扩展(JMX)、J2EE 连接器架构(JCA)和 Java 命名和目录接口(JNDI) Java 语言标准。使用 JMX,您可以在 Hibernate 运行时配置它。Hibernate 可能被部署为 JCA 连接器,您可以使用 JNDI 在应用中获得 Hibernate 会话工厂。此外,Hibernate 使用标准的 Java 数据库连接(JDBC)数据库驱动程序来访问关系数据库。Hibernate 没有取代 JDBC 作为数据库连接层;Hibernate 位于 JDBC 之上。
除了标准的 Java APIs,许多 Java web 和应用框架现在都与 Hibernate 集成在一起。Hibernate 简单、干净的 API 使得这些框架很容易以某种方式支持 Hibernate。Spring 框架提供了优秀的 Hibernate 集成,包括对持久化对象的通用支持、一组通用的持久化异常和事务管理。第十二章解释了如何在 Spring 应用中配置 Hibernate。
不管您将 Hibernate 集成到什么环境中,某些需求是不变的。您需要定义适用的配置详细信息;然后这些由 ServiceRegistry 对象表示。从 ServiceRegistry 对象创建一个 SessionFactory 对象;由此,会话对象被实例化,应用通过它访问 Hibernate 的数据库表示。
部署 Hibernate
将 Hibernate 集成到应用中需要两组组件:数据库驱动程序和 Hibernate 依赖项本身。
本书的示例代码使用 H2 作为一个小型的嵌入式数据库; 3 这可以在 http://h2database.com/ 找到。这并不是说其他数据库不如 H2 有价值,这只是一个权宜之计;H2 的同类项目 HSQLDB 也是可行的,Derby 也是如此;如果您手边有 MySQL 或 PostgreSQL 数据服务器,它们也可以工作,但是嵌入式数据库意味着您不必运行外部进程,也不必配置特殊的数据库或用户帐户。 4 H2 还提供了一个非常方便的基于 web 的控制台,你可以用它与数据库(或者任何数据库,如果你提供了类路径的驱动程序的话)进行交互,如果你需要那种东西的话。
如果您正在使用 Hibernate 二进制文件下载(通过 https://hibernate.org/orm/releases/ ,从一个“发布包”),为了使用 Hibernate,lib/required目录中包含的所有 jar 都是必需的。
也许集成 Hibernate 的一个更简单的方法是使用构建工具,比如 Gradle ( www.gradle.org/ ,Hibernate 项目本身使用的)、SBT ( www.scala-sbt.org/ )或 Maven ( http://maven.apache.org/ ),后者可以说是最流行的构建工具,如果不是最好的。 6
所有这些构建工具都能够将依赖项捆绑到可交付的工件中。它们还能够包含过渡性的依赖项,这意味着依赖于给定子项目的项目也会继承该子项目的依赖项。
我们将 Maven 作为本书其余部分的构建环境;其他构建工具的用户通常能够相当容易地从 Maven 进行迁移。 7
安装 Maven
安装 Maven 的方法有很多。这是一个粗略的概述;不同的操作系统(以及不同的系统配置)会影响安装过程,因此当您有疑问时,可以参考 http://maven.apache.org/download.cgi#Installation 获取实际文档。
不过,为了节省您的时间,您可以从 http://maven.apache.org/download.cgi/下载 Maven你应该得到最新的版本。UNIX 用户(包括 Linux 和 MacOS 用户)要下载以 tar.gz 结尾的文件;Windows 用户应该下载 zip 文件。
在 UNIX 中,将文件解压缩到您选择的目录中;可能运行的命令示例如下:
mkdir ~/tools || cd ~/tools; tar xf apache-maven-3.8.1-bin.tar.gz
这会创建~/tools/apache-maven-3.8.1/,mvn 可执行文件会在~/tools/apache-maven-3.8.1/bin;将此添加到您的命令路径中。
对于 Windows,打开归档文件并将其解压缩到一个已知的位置(如C:\tools\)。通过系统属性对话框将mvn.bat(在本例中是C:\tools\apache-maven-3.8.1\bin)的位置添加到您的路径中,您应该能够在命令提示符下运行带有“mvn”的 Maven。
Maven 使用一个项目对象模型,通常用 XML 编写,称为“pom.xml”。该文件描述了项目的名称、版本和构建配置(如果有的话),以及任何子项目和任何项目依赖项。当 Maven 运行时,它会自动下载它需要的任何资源,以便按照指定完成构建,然后它会编译项目源代码;如果项目包含测试,那么当(且仅当)没有测试失败发生时,它将运行测试并完成构建。
本书使用一个父项目,该项目包含本书的全局依赖项以及与章节对应的子项目;许多操作代码是作为子项目中的一组测试编写的。例如,第一章使用了两种方法向数据库写入数据和从数据库读取数据;那些测试被写成 TestNG?? 8测试类:chapter01.hibernate.PersistenceTest和chapter01.jdbc.PersistenceTest。
在编写了第章 1 之后,父项目的配置文件看起来如清单 2-1 所示。
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<modelVersion>4.0.0</modelVersion>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>hibernate-6-parent</artifactId>
<packaging>pom</packaging>
<version>5.0</version>
<modules>
<module>chapter01</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<testng.version>7.4.0</testng.version>
<hibernate.core.version>6.0.0.Alpha8</hibernate.core.version>
<h2.version>1.4.200</h2.version>
<logback.version>1.2.3</logback.version>
<lombok.version>1.18.18</lombok.version>
<hibernate.validator.version>
6.2.0.Final
</hibernate.validator.version>
<javax.el-api.version>3.0.0</javax.el-api.version>
<ignite.version>2.10.0</ignite.version>
<jackson.version>2.12.3</jackson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.core.version}</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-core</artifactId>
<version>${ignite.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
<version>${hibernate.core.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate.core.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
Listing 2-1The Top-Level pom.xml
这指定了关于项目的许多事情(比如 Java 版本,这是 Java 9 的当前维护版本),包括四个依赖项:Hibernate 本身;H2 数据库;一个日志框架,名为“log back”;以及 TestNG,最后一个仅限于测试阶段(按照“scope”节点的指示)。
子项目——在这个清单中,这只是第一章——将自动接收这个配置和它的依赖集,这意味着我们不必经常重复。
要在安装 Maven 后构建并运行这个项目,您只需转到包含 pom.xml 的目录并执行mvn package——如上所述,这将下载所有需要的依赖项,构建它们,按顺序测试项目,并为每个项目构建可部署的构件,无论是作为 jar 文件还是任何其他类型的可部署单元。
Maven 项目有一个特定的文件夹布局,尽管它是可配置的;默认情况下,Java 编译器编译在src/main/java中找到的所有代码,Maven 将编译后的类和src/main/resources中的任何内容捆绑到可交付的工件,即正在构建的库或包中。src/test/java目录包含 Java 中的测试类,这些测试类随后被编译并运行,类路径由测试、src/test/resources中的资源以及类路径中src/main中的任何内容构建而成。
哇,有很多关于非 Hibernate 的讨论——所有这些都可以在每个给定构建环境的网站上找到(并被颠覆)。总的来说,你可以(也应该)用你喜欢的;这本书使用 Maven 是因为它很常见,而不是因为它是真正的构建工具。
让我们看看到目前为止我们运行的实际代码,并对其进行解释。这将为你将来的讨论提供一个基础,即使你在本章之外不会用到它。
我们已经提到了顶级的pom.xml文件;我们将从chapter02目录开始(它几乎是chapter01目录的克隆,除了用chapter02代替chapter01——我们很快就会看到这一变化)。我们的项目描述文件(我们的pom.xml)非常简单,只指定了父项目和当前项目的名称(参见清单 2-2 )。
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>hibernate-6-parent</artifactId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter02</artifactId>
</project>
Listing 2-2Chapter 2’s Project Object Model
我们的Message.java在src/main/java/chapter02/hibernate/Message.java举行。这与清单 1-7 中的 POJO 基本相同,只是被重命名并放在不同的包中。既然其他都一样,这里就不一一列举了。
我们实际运行的代码在 src/test 目录下,由两个相关文件组成: 10 src/test/java/chapter02/hibernate/PersistenceTest.java和src/test/resources/hibernate.cfg.xml。
我们已经看过第一章中的PersistenceTest.java,但是让我们再看一遍,更详细一点。
package chapter02.hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class PersistenceTest {
private SessionFactory factory = null;
@BeforeClass
public void setup() {
StandardServiceRegistry registry =
new StandardServiceRegistryBuilder()
.configure()
.build();
factory = new MetadataSources(registry)
.buildMetadata()
.buildSessionFactory();
}
public Message saveMessage(String text) {
Message message = new Message(text);
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
session.persist(message);
tx.commit();
}
return message;
}
@Test
public void readMessage() {
Message savedMessage = saveMessage("Hello, World");
List<Message> list;
try (Session session = factory.openSession()) {
list = session
.createQuery("from Message", Message.class)
.list();
}
assertEquals(list.size(), 1);
for (Message m : list) {
System.out.println(m);
}
assertEquals(list.get(0), savedMessage);
}
}
Listing 2-3chapter02/src/test/chapter02/hibernate/PersistenceTest.java
setup()方法是 Hibernate 初始化的地方。Hibernate 从SessionFactory类型(这里称为factory)获取Session对象——这些对象执行实际的数据库交互;它从服务注册中心获取SessionFactory。在我们的测试中,我们显式地构建了服务注册中心;如果您使用的是 Spring 或 Jakarta EE 之类的东西,那么SessionFactory可能会作为应用启动的一部分被初始化,您只需为它请求一个值。
然而,我们不会将Session引用存储很久。它们很像数据库连接;如果你需要一个,你就买一个,用完后马上扔掉。在某些情况下,这对你如何编写你的应用有真正的影响。 11
如果愿意,您可以向StandardServiceRegistryBuilder().configure()提供一个资源名称;缺省值是hibernate.cfg.xml,但是如果您想要明确地使用不同的配置——例如,为了测试的目的——这是您应该提供配置名称的地方:
StandardServiceRegistry registry =
new StandardServiceRegistryBuilder()
.configure("my-special-hibernate.cfg.xml")
.build();
看看使用Session的方法,我们可以在一个Session本身上使用自动资源管理;try-with-resources 要求类型上存在一个close()方法。有一种方法我们可以伪造它,这样我们可以自动尝试提交事务,这可能在像这样的简单代码中起作用(事务失败的条件非常有限,就此而言,对测试来说是灾难性的),但通常您的代码会想要明确决定是否提交事务。我们将在第八章中更全面地介绍事务。
拼图的最后一块是实际的配置文件本身,它位于 src/test/resource/hibernate . CFG . XML 中。
<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:./db2</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create-drop</property>
<mapping class="chapter02.hibernate.Message"/>
</session-factory>
</hibernate-configuration>
Listing 2-4chapter02/src/test/resources/hibernate.cfg.xml
这个文件可以作为每个 Hibernate 配置的样板文件。在其中,我们指定了 JDBC 驱动程序类;用于访问数据库的 JDBC URL、用户名和密码;方言(允许 Hibernate 为每个给定的数据库正确地产生 SQL 一些配置,比如是否将生成的 SQL 转储到控制台;以及对模式做什么。最后,它指定了应该管理的类——在本例中,只有我们的Message类。
从这个文件中我们可以控制很多事情;我们甚至可以用它来指定我们的对象到数据库的映射(即,忽略我们到目前为止一直在使用的注释)。在本书后面的章节中,你会看到更多关于如何做到这一点的内容;在将现有的数据库模式 12 映射到对象模型时,它有很大的帮助。
大多数编码人员会(也应该)更喜欢基于注释的映射。
连接池
正如您所看到的,Hibernate 使用 JDBC 连接来与数据库交互。创建这些连接是很昂贵的——可能是 Hibernate 在典型用例中执行的最昂贵的一个操作。
由于 JDBC 连接管理非常昂贵,您可以将连接放在池中,这样可以提前打开连接并重用它们(只在需要时关闭它们,而不是“当它们不再被使用时”)。
幸运的是,Hibernate 被设计为默认使用连接池,这是一个内部实现。然而,Hibernate 的内置连接池并不是为生产使用而设计的。在生产中,您可以通过使用 JNDI(Java 命名和目录接口)提供的数据库连接或通过参数和类路径配置的外部连接池来使用外部连接池。
Hibernate 被设计成能够使用任何数量巨大的可用数据库池。如果可以,它将尝试使用给定的连接池;实际上,这就像将连接池实现放在类路径中一样简单。如果类路径中有多个连接池,它会遵循一个相当简单的算法来确定使用哪个——如果您试图配置一个特定的连接池,它将使用该连接池,否则将使用“sane 缺省值”。
将连接池放在 Hibernate 的类路径中的正确方法是,简单地将它作为一个依赖项包含进来。例如,对于 HikariCP,它是 Maven 中一个简单的<dependency>块,它本身属于<dependencies>块。
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-hikaricp</artifactId>
<version>${hibernate.core.version}</version>
</dependency>
</dependencies>
Listing 2-5Changes for the Object Model to Include HikariCP
它实际上支持五个不同的连接池,除了它的内部连接池和 JNDI 提供的池:HikariCP(如上所述!),c3p0,普罗肖尔维布尔 DBCP 和 Agroal。有类似于hibernate-hikaricp工件的东西支持其中的每一个;如果通过使用配置元素在类路径中包含多个实现,您可以选择希望显式使用哪一个。 13
这些当中,哪一个最好?这真的不是一个要回答的小问题;大多数连接池都有自己的特点,哪一个最适合特定的应用实际上取决于确切地说需要满足什么需求。总的来说,它们都工作得很好,达到了预期的目的。出于本书的目的,即使 Hibernate 的连接池也足够了;在任何时候都没有引入严重到足以担心资源匮乏的连接压力。然而,如果你让我推荐的话,我会推荐 HikariCP,它在尺寸和性能上有很好的平衡,如果 JNDI 连接不可用的话。
使用 JNDI
如果您在 Java EE 环境中使用 Hibernate——例如,在 web 应用中——那么您需要配置 Hibernate 来使用 JNDI。JNDI 连接池由容器管理(因此由部署者控制),这通常是在分布式环境中管理资源的“正确方法”。
例如,wildly(http://wildfly.org/)预装了一个示例数据源,名为“Java:JBoss/data sources/examples”这是一个 H2 数据库,所以方言已经是正确的;新的配置看起来类似于清单 2-6 中所示的内容。
<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="jndi.url">java:jboss/datasources/ExampleDS</property>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create-drop</property>
<mapping class="chapter02.hibernate.Message"/>
</session-factory>
</hibernate-configuration>
Listing 2-6Hibernate Configured to Use JNDI for the Data Source
理想情况下,不会使用 java:jboss 树;您将在 java:comp/env 树中使用应用组件范围内的名称。 14
摘要
在这一章中,我们简要介绍了如何使用 Maven 来构建和测试你的项目,以及如何指定依赖关系。我们还展示了 TestNG 作为运行代码的简单工具的用法。最后,我们解释了如何配置 Hibernate,从获取 SessionFactory 开始,以 SessionFactory 的配置结束,涵盖了 Hibernate 中包含的简单 JDBC 连接管理、连接池的使用以及使用 JNDI 来获取数据库连接。
现在,您应该有了足够的工具,可以专注于使用 Hibernate 来帮助您管理持久对象模型。我们将根据需要在示例代码中添加更多细节。
在下一章,我们将构建一些稍微复杂一点的(也是有用的)对象模型来说明 Hibernate 的更多核心概念。
三、构建简单的应用
在这一章中,我们将创建一个应用的外壳,这将允许我们演示一些使用 Hibernate 的系统中常见的概念。我们将涵盖以下内容:
-
对象模型设计,包括对象之间的关系
-
查看和修改持久数据的操作(插入、读取、更新和删除)
通常,我们会使用一个服务层来封装一些操作,事实上,随着我们的继续,我们会添加一个服务层,但是此时我们想了解更多如何与 Hibernate 本身进行交互。这里的目标是不要把时间浪费在一个“可以扔掉的”示例应用上我们肯定不会有一个完整而理想的代码库,但它将是一个在现实世界中如何使用 Hibernate 的模型。
当然,这种说法有一个警告:不同的应用和架构师有不同的方法。这只是创建这种应用的一种方式;其他人会采取和这个一样有效的不同方法。
此外,我们的模型将是渐进的,这意味着它的质量不会很高。我们将继续介绍各种新概念;我们将有很多机会回到以前编写的代码并对其进行改进。
一个简单的应用
我们试图创建的是一个应用,允许在各种技能领域的同行排名。
这个概念大概是这样的:John 认为 Tracy 非常擅长 Java,所以在 1 到 10 的范围内,他给 Tracy 打了 7 分。萨姆认为特蕾西很正派,但并不伟大;他会给特蕾西 5 分。根据这两个排名,人们可能会猜测 Tracy 在 Java 中是 6。实际上,这样一个小样本集,你将无法判断这个排名是否准确,但在 20 个这样的排名之后,你将有机会得到一个真正合法的同行评估。
因此,我们想要的是一种方式,让观察者为特定的人提供给定技能的排名。我们还想要一种方法来确定每个人的实际排名,以及一种方法来找出谁对于给定的技能排名“最好”。
如果你着眼于应用设计来看这些段落,你会看到我们有四种不同类型的实体——数据库中要管理的对象——和一些服务。
我们的实体是这些:人(是观察者和主体;因此,我们可以使用一个既可以指观察者又可以指主题的单一类型)、技能和等级。
我们的关系看起来像这样:
一个主体——一个人——拥有零项、一项或多项技能。一个人的技能各有零个、一个或多个排名。
排名有一个分数(“在 1 到 10 的范围内”)和一个观察者(提交特定排名的人)。
关系和基数
在我们开始深入研究对象模型之前,有必要回顾一下数据库术语中关系是如何指定的。
考虑一个人和一个联邦身份号码。一个人可能没有联邦身份证,所以考虑两个数据库表是有意义的(一个Person表和一个FIN表,代表“联邦身份证号”)。我们可以将这表达为“一对零或一”的关系,这意味着一个Person记录可以有零个或一个FIN记录。我们还可以从FIN表的角度来表达这种关系,例如FIN与Person具有“一对一”的关系,这意味着每一个FIN记录都与一个且恰好一个Person记录相关。
您看到的关系类型通常属于以下几组:
-
一对一,或者 1:1。在这种情况下,关系的双方都只有一个记录或实体。在 Hibernate 中,这是一个被标记为而不是可选的关系。
-
一到零或者一个。在这种关系下,“目的地”记录——“0 或 1”是可选的,但在其他方面它符合 1:1 的关系。
-
一对多,或 1:M。这可能显示为一个
Person和他们的BankAccount记录之间的关系,例如,因为一个人可能有储蓄账户、支票账户和循环贷款。 -
多对多,或 M:M,有了这种结构,关系双方的基数都很高;您可以在这里想象一个由
SchoolCourse和Student组成的结构,因为每个学生可以注册许多不同的课程,并且每个课程可以有许多学生。通常,这不是一个特别有效的结构,在实践中——你更可能有一个学校课程有许多Schedule记录,代表在特定时间注册的每个学生,每个Student也有多个Schedule记录,这意味着Course到Schedule是一个 1:M 关系,正如Student和Schedule之间的关系一样。 -
多对一或 M:1 是 1:M 的逆表达式,用于表示对另一个实体类型的依赖性。
我们将在本章和其他章节中讨论这些关系类型的使用。
第一次尝试
我们的项目将允许我们写、读和更新不同科目的排名,并告诉我们谁在某项技能上的平均分数最高。
起初,它不会非常有效地完成这些事情,但是随着时间的推移,我们将实现我们对(某种程度上)敏捷开发实践的渴望,并且我们将学习相当多的关于如何使用 Hibernate 读写数据的知识。
像往常一样,我们将使用测试驱动开发。让我们写一些测试,然后试着让它们通过。我们最初的代码将非常原始,只测试我们的数据模型,但最终我们将测试服务。
我们的数据模型如下所示。正如您所看到的,它有三种对象类型和三种关系:一个人以两种方式与一个排名相关联(作为主题和观察者),每个排名都有一个关联的技能。

图 3-1
简单的实体关系图
可能值得指出的是,这种数据模型并不理想。就目前而言,这没什么——我们正在努力构建一些东西,为我们提供一个起点,我们将在前进的过程中考虑我们的全部需求。
我们也不可否认地低估了我们的实体。例如,一个人可以不仅仅是一个名字。(一个人也可以是一个数字,对吗?…哦,等等,这一点也不好笑,因为我们最终将为每个人添加一个数字标识符作为人工密钥。)也许我们会在开发模型时解决这个问题和其他问题。
所以让我们从设计我们的对象开始。
因为我们的问题描述集中在一个人的概念上(作为主体和观察者),让我们从那个开始。可以表示一个人的最简单的 JavaBean 可能看起来像清单 3-1 。
package chapter03.simple;
public class Person {
String name;
public Person() {}
public void setName(String name) { this.name=name; }
public String getName() { return name; }
}
Listing 3-1A POJO Representing Our Person Object
请注意,这些类的“简单”版本在本书的源代码中并不存在,因为它们实际上并没有贡献价值,并且我们将要编写的内容也不会用到它们。你卑微的作者试图找出一种方法来有效地表示代码库编写时的不同阶段,但是失败了;能有所帮助,但在书中表现出来却是一场噩梦。
为了简洁起见,从现在开始我们将忽略简单的赋值函数和访问函数(分别是Person类中的setName()和getName()),除非我们需要包含它们。这里我们也将忽略toString()、equals()和hashCode()的实现,尽管本章的示例代码中有这样的例子。
这个Person实现只包含了一个Person的概念,忽略了其他的对象类型。让我们看看他们长什么样,这样我们就可以重访Person,可以说是充实它。
Skill类看起来几乎和Person类一模一样,这是应该的;它们可以从一个公共基类继承,但是现在让我们把它们完全分开,如清单 3-2 所示。
package chapter03.simple;
public class Skill {
private String name;
public Skill() {}
}
Listing 3-2A POJO Representing Our Skill Object
Ranking类稍微复杂一点,但也没复杂多少。实际上,它所做的只是编码 UML 中显示的关联的一个方面。值得注意的是,当我们设计我们的对象时,我们根本不必考虑数据库关联;一个Ranking有一个匹配主题的属性,所以这就是它所使用的。此时我们只需要考虑对象之间的关系,因为 Hibernate 可以帮助我们映射数据库中的关系。看看清单 3-3 。
package chapter03.simple;
public class Ranking {
private Person subject;
private Person observer;
private Skill skill;
private Integer ranking;
public Ranking() { }
// accessors and mutators omitted for brevity
}
Listing 3-3A POJO Representing Our Ranking Object
写入数据
至此,我们有了一个完全可用的 Java 数据模型。我们可以使用这种数据模型(稍加修改以包含清单中没有包含的赋值函数和访问函数)来创建代表Person类型、Skill类型和Rankings的实体;我们可以使用关联来提取足够的数据以满足我们的需求。创建我们的数据模型可能如清单 3-4 所示。
package chapter03.simple;
import org.testng.annotations.Test;
public class ModelTest {
@Test
public void testModelCreation() {
Person subject=new Person();
subject.setName("J. C. Smell");
Person observer=new Person();
observer.setName("Drew Lombardo");
Skill skill=new Skill();
skill.setName("Java");
Ranking ranking=new Ranking();
ranking.setSubject(subject);
ranking.setObserver(observer);
ranking.setSkill(skill);
ranking.setRanking(8);
// just to give us visual verification
System.out.println(ranking);
}
}
Listing 3-4A Test That Populates a Simple Model
然而,能够使用数据模型并不等同于能够持久化或查询数据模型。这是了解数据模型如何工作的良好开端,但还没有到实际使用它的地步。
为了让 Hibernate 与我们的模型一起工作,我们将首先通过用@Entity注释标记它来将Person对象转换成一个实体。 1 接下来,我们将名称标记为数据模型的列(带有@Column),然后我们将添加一个人工键——一个唯一的标识符——以允许我们使用名称以外的东西作为主键。
我们将在后面描述更多关于@Id和@GeneratedValue注释的内容;目前,这将属性标记为由数据库自动生成的唯一主键。密钥生成的形式将取决于数据库本身。(在这种情况下,密钥生成将使用数据库序列。这可能不是你想要的;这也是你可以控制的。)
Person 对象现在看起来类似于清单 3-5 中所示的内容。
package chapter03.hibernate;
import javax.persistence.*;
import java.util.Objects;
@Entity
public class Person {
@Column(unique = true)
private String name;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
public Person() {
}
}
Listing 3-5src/main/java/chapter03/hibernate/Person.java
请注意,我们在这里没有显示Person的完整源代码;源代码还包括变异函数、访问函数、toString()、equals()和hashCode()。
现在,我们可以创建一个将实例写入数据库的测试。这里有一段用于此目的的代码。同样,我们将在未来的迭代中对这段代码进行相当多的重构;参见清单 3-6 。
package chapter03.hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
public class PersonTest {
SessionFactory factory;
@BeforeClass
public void setup() {
StandardServiceRegistry registry =
new StandardServiceRegistryBuilder()
.configure()
.build();
factory = new MetadataSources(registry)
.buildMetadata()
.buildSessionFactory();
}
@Test
public void testSavePerson() {
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Person person = new Person();
person.setName("J. C. Smell");
session.save(person);
tx.commit();
}
}
}
Listing 3-6src/test/java/chapter03/hibernate/PersonTest.java
这是我们在第 1 和 2 章节中的Message示例的近似镜像,做了一些修改以反映我们正在保存一个Person而不是一个Message,正如人们可能预期的那样。
实际测试非常简单。它创建了一个Person,除了持久化它,它什么也不做。我们甚至没有试图验证它的持久化——我们只是在运行持久化机制。假设是这种情况(确实如此),我们也可以假设相同的代码适用于Skill对象;但是Ranking对象——及其关联——还需要一点工作。
在编写一个Ranking对象之前,我们需要考虑的一件事是如何找到我们的一个实体。首先,这种能力将在简单的持久化测试中帮助我们:验证不仅执行了save()方法,而且它实际上也持久化了我们的数据。另一方面,在testSavePerson()代码中,当我们知道Person不存在时,我们正在创建一个Person;然而,对于Ranking,我们完全期望重用Person实例以及Skill实例。
所以我们需要创建一个机制来查询我们的数据库。我们将创建一个方法,使用查询从会话中返回一个Person引用;我们将在未来重新审视查询机制,对其进行一些优化。
完善数据模型
为了清楚起见,本章的其他实体如下:Ranking和Skill。我们会在后面提到这些。
package chapter03.hibernate;
import javax.persistence.*;
@Entity
public class Skill {
@Column
private String name;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
public Skill() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Skill{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
Listing 3-8src/main/java/chapter03/hibernate/Skill.java
package chapter03.hibernate;
import javax.persistence.*;
@Entity
public class Ranking {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne
private Person subject;
@ManyToOne
private Person observer;
@ManyToOne
private Skill skill;
@Column
private Integer ranking;
public Ranking() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Person getSubject() {
return subject;
}
public void setSubject(Person subject) {
this.subject = subject;
}
public Person getObserver() {
return observer;
}
public void setObserver(Person observer) {
this.observer = observer;
}
public Skill getSkill() {
return skill;
}
public void setSkill(Skill skill) {
this.skill = skill;
}
public Integer getRanking() {
return ranking;
}
public void setRanking(Integer ranking) {
this.ranking = ranking;
}
@Override
public String toString() {
return "Ranking{" +
"id=" + id +
", subject=" + subject +
", observer=" + observer +
", skill=" + skill +
", ranking=" + ranking +
'}';
}
}
Listing 3-7src/main/java/chapter03/hibernate/Ranking.java
这两个类都还有改进和完善的空间——它们不包括equals()和hashCode(),toString()甚至还没有做得很好——但这对于本书的这个阶段来说已经足够了。
阅读日期
清单 3-9 是查找给定名称的Person的代码。这个代码片段使用了 Hibernate 查询语言(HQL),它与 SQL 有着松散的联系;我们将在后面的章节中看到更多关于 HQL 的内容。
private Person findPerson(Session session, String name) {
Query<Person> query = session.createQuery(
"from Person p where p.name=:name",
Person.class
);
query.setParameter("name", name);
Person person = query.uniqueResult();
return person;
}
Listing 3-9A Method to Find a Specific Person
这段代码声明了对org.hibernate.query.Query ( https://docs.jboss.org/hibernate/orm/6.0/javadocs/org/hibernate/query/Query.html )的引用,它构建了一个 SQL select 语句的粗略模拟。这种形式的查询从从Person实体创建的表中选择数据(该实体可能有也可能没有表名“person”),别名为“p”,仅限于其“name”属性等于命名参数(称为“name”)的对象。它还指定了查询的引用类型(用Person.class),以减少类型转换和不正确返回类型的潜在错误。 2
然后,我们将参数值“name”设置为我们要搜索的名称。
因为此时我们只对一个可能的匹配感兴趣(这是我们目前实现的一个限制),所以我们返回一个唯一的结果:单个对象。如果我们的数据库中有五个同名的记录,将会抛出一个异常;我们可以通过使用query.setMaxResults(1)并返回query.list()中的第一个(也是唯一的)条目来解决这个问题,但是解决这个问题的正确方法是弄清楚如何非常具体地返回正确的Person。
如果没有找到结果,将返回一个信号值-null。3
精明的读者(因此,他们所有人)会注意到我们传递了一个Session给这个方法,并且这个方法被声明为private。这是为了让我们更干净地管理资源;我们正在构建微小的功能块,我们不希望每一个微小的功能都经历一个获取资源的过程。我们期望调用者将管理Session,并暗示会影响该方法的事务。如果我们需要公开这个方法的一个版本,它不会给调用者增加会话管理的负担,我们可以重载这个方法名——我们也会这么做。(这个方法实际上是专门为我们服务中的其他方法而设计的——这些方法是那些期望获得Session并管理事务的方法。)
我们现在可以编写一个findPerson()方法,如果存在一个同名的Person,则返回该名称,如果没有找到,则创建一个新的Person对象;参见清单 3-10 。
private Person savePerson(Session session, String name) {
Person person = findPerson(session, name);
if (person == null) {
person = new Person();
person.setName(name);
session.save(person);
}
return person;
}
Listing 3-10A Method to Create or Return a Specific Person
我们构建一个Ranking(在RankingTest中)的第一个代码片断可能看起来类似于清单 3-11 中所示。
这个方法假设了一个正在工作的saveSkill()方法,这个方法还没有展示出来;我们将很快展示整个类的,包括每一个方法。
@Test
public void testSaveRanking() {
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Person subject = savePerson(session, "J. C. Smell");
Person observer = savePerson(session, "Drew Lombardo");
Skill skill = saveSkill(session, "Java");
Ranking ranking = new Ranking();
ranking.setSubject(subject);
ranking.setObserver(observer);
ranking.setSkill(skill);
ranking.setRanking(8);
session.save(ranking);
tx.commit();
}
}
Listing 3-11A Method to Test Creating a Ranking
章节代码按照原样对这个方法进行了编码,但是这个方法也为我们提供了另一个方法的开端, 4 这个方法抽象了所有重复的代码,这样我们就可以提供四条重要的信息并非常快速地生成数据。
记住这一点,让我们再次看看查询。我们已经展示了查询可以返回单个结果;让我们来看看按顺序返回多个结果的查询,要知道在许多方面,我们离高效甚至正确还很远。
我们的需求之一是能够确定给定的Skill对给定的Person的排序。让我们编写另一个测试作为概念证明。
首先,我们将编写一个方法,为 J. C. Smell 增加几个排名;我们已经展示过他在 Java 中有一个 8。让我们加上一个 6 和一个 7,很明显,他的平均技能是 7。这样,我们的测试方法可能看起来如清单 3-12 所示。
@Test
public void testRankings() {
populateRankingData();
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:name "
+ "and r.skill.name=:skill", Ranking.class);
query.setParameter("name", "J. C. Smell");
query.setParameter("skill", "Java");
IntSummaryStatistics stats = query.list()
.stream()
.collect(
Collectors.summarizingInt(Ranking::getRanking)
);
long count = stats.getCount();
int average = (int) stats.getAverage();
tx.commit();
session.close();
assertEquals(count, 3);
assertEquals(average, 7);
}
}
private void populateRankingData() {
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
createData(session, "J. C. Smell", "Gene Showrama", "Java", 6);
createData(session, "J. C. Smell", "Scottball Most", "Java", 7);
createData(session, "J. C. Smell", "Drew Lombardo", "Java", 8);
tx.commit();
}
}
private void createData(Session session,
String subjectName,
String observerName,
String skillName,
int rank) {
Person subject = savePerson(session, subjectName);
Person observer = savePerson(session, observerName);
Skill skill = saveSkill(session, skillName);
Ranking ranking = new Ranking();
ranking.setSubject(subject);
ranking.setObserver(observer);
ranking.setSkill(skill);
ranking.setRanking(rank);
session.save(ranking);
}
Listing 3-12A Method to Test Ranking Operations
testRanking()方法使用了一个稍微高级一点的查询:该查询遍历来自Ranking对象的属性树,以匹配主题名称和技能名称。有了我们的对象模型中的实体引用,不需要了解特定的数据库语法或功能就可以很容易地进行SQL JOIN;Hibernate 负责为我们编写所有的 SQL,我们可以“自然地”使用这些对象
顺便说一下,这不是查询工具的一个特别好的用途;随着我们的进展,我们将会反复讨论它,特别是在本章的最后一节,我们将使用 Hibernate 的查询功能来完成所有计算平均值的工作。 5
更新数据
如果我们想改变数据呢?假设 Gene Showrama 在我们的示例代码中将 J. C. Smell 在 Java 中排名为 6,他意识到自己已经改变了看法。让我们看看我们必须做些什么来更新数据。
首先,让我们将排名平均值计算例程重构为一个可重用的方法。接下来,我们将编写测试来更新数据,然后重新计算平均值,测试它以确保我们的数据被正确持久化。参见清单 3-13 。
private int getAverage(String subject, String skill) {
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:name "
+ "and r.skill.name=:skill", Ranking.class);
query.setParameter("name", subject);
query.setParameter("skill", skill);
IntSummaryStatistics stats = query.list()
.stream()
.collect(
Collectors.summarizingInt(Ranking::getRanking)
);
int average = (int) stats.getAverage();
tx.commit();
return average;
}
}
@Test
public void changeRanking() {
populateRankingData();
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:subject and "
+ "r.observer.name=:observer and "
+ "r.skill.name=:skill", Ranking.class);
query.setParameter("subject", "J. C. Smell");
query.setParameter("observer", "Gene Showrama");
query.setParameter("skill", "Java");
Ranking ranking = query.uniqueResult();
assertNotNull(ranking, "Could not find matching ranking");
ranking.setRanking(9);
tx.commit();
}
assertEquals(getAverage("J. C. Smell", "Java"), 8);
}
Listing 3-13A Method to Test Ranking Operations
我们在这里做什么?在我们用已知值填充数据之后,我们将构建一个查询来定位我们想要更改的特定的Ranking(Java 上的“J. C. Smell”的一个Ranking,由“Gene Showrama”编写)。我们检查以确保我们有一个有效的Ranking——这是应该的,因为该数据是由我们的populateRankingData()方法创建的——然后我们做一些非常奇怪的事情。
我们设置一个新的排名分数,用ranking.setRanking(9); …就这样。我们提交当前事务,并让会话关闭,因为我们已经完成了它。
Hibernate 观察数据模型,当某些东西发生变化时,它会自动更新数据库以反映这些变化。 6 事务提交对数据库的更新,以便其他会话——包含在我们很快将看到的findRanking()方法中——可以看到它。
对此有一些警告(当然还有变通办法)。当 Hibernate 为您加载一个对象时,它就是一个“托管对象”——也就是说,它是由那个会话管理的。突变(更改)和访问通过一个特殊的过程将数据写入数据库,或者如果会话尚未加载数据,则从数据库中提取数据,因为有些数据可能无法自动检索。(例如,一个对象可能有一个大的二进制对象,我们不希望每次检索该实体时都加载它。这里的代理将在具体访问实体时加载它,而不是在第一次检索实体时。)我们称这个对象处于“持久状态”,这就引出了一个概念,当我们在 Java 中使用持久化时,这个概念将变得很重要。 7
持久化上下文
与会话相关的对象有四种状态:持久、暂时、分离或删除。
当我们创建一个新对象时,它是短暂的——也就是说,Hibernate 没有给它分配标识符,数据库也不知道这个对象。这并不意味着数据库可能没有数据。想象一下,如果我们在 Java 上为 Gene Showrama 的 J. C. Smell 手动创建一个Ranking。新的排序在数据库中有一个类似物,但是 Hibernate 不知道内存中的对象与数据库中的对象表示是等价的。
当我们在一个新对象上调用save()时,我们将它标记为“持久的”,当我们查询一个对象的会话时,它也处于持久状态。更改反映在当前事务中,在提交事务时写入。这就是在changeRanking()中发生的事情——我们正在改变一个处于持久状态的对象,当事务被提交时,对处于持久状态的对象的任何更改都会将其更改写入数据库。我们可以通过使用Session.merge()将一个瞬态对象转换成一个持久对象,这个我们还没有见过(但是我们会的)。
分离的对象是一个持久对象,它的会话已经被关闭,或者已经被从Session中逐出。在我们更改Ranking的例子中,当会话关闭时,我们更改的Ranking对象对于findRanking()调用来说处于分离状态,即使我们从数据库中加载了它,并且它曾经处于持久状态。
移除的对象是在当前事务中标记为删除的对象。当对该对象引用调用Session.delete()时,该对象变为移除状态。请注意,处于已删除状态的对象在数据库中被删除,但不会在内存中被删除,就像对象可以存在于数据库中而没有内存中的表示一样。
删除数据
我们最不希望看到的是如何删除数据,或者更确切地说,如何将数据移动到持久化上下文的删除状态——这几乎是一回事。(直到事务被提交,它才真正被“删除”,即使这样,内存中的表示也是可用的,直到它超出范围,正如我们在“删除状态”一节中所描述的那样)
举例来说,Gene Showrama 已经意识到他确实没有足够的信息来为 J. C. Smell 在 Java 上提供有效的排名,所以他希望删除它。这个代码与我们的更新非常相似:我们将找到Ranking,然后调用Session.delete()。
我们可以重构寻找Ranking(来自changeRanking()测试)的机制,这将给我们一个处于持久状态的Ranking。然后,我们通过会话删除它,并提交更改;然后,我们可以请求新的平均值,看看我们的更改是否反映在数据库中。
我们的代码如清单 3-14 所示。
private Ranking findRanking(Session session,
String subject, String observer, String skill) {
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:subject and "
+ "r.observer.name=:observer and "
+ " r.skill.name=:skill", Ranking.class);
query.setParameter("subject", subject);
query.setParameter("observer", observer);
query.setParameter("skill", skill);
Ranking ranking = query.uniqueResult();
return ranking;
}
@Test
public void removeRanking() {
populateRankingData();
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Ranking ranking = findRanking(session, "J. C. Smell",
"Gene Showrama", "Java");
assertNotNull(ranking, "Ranking not found");
session.delete(ranking);
tx.commit();
}
assertEquals(getAverage("J. C. Smell", "Java"), 7);
}
Listing 3-14Removing a Ranking
这就像魔术一样,除了它不是:它只是 Hibernate 管理数据库,以反映我们向它显示的变化。
关于事务的注释
我们也多次提到“事务”,在每个会话引用中都使用它们。那么它们是什么呢?
事务是数据库的“捆绑工作单元”。 8
当您启动一个事务时,您说您希望看到数据库在某个时间点(“现在”)的状态,并且任何修改只影响从该起始点开始存在的数据库。
更改是作为一个整体提交的,因此在事务完成之前,其他事务看不到它们。这意味着事务允许应用定义离散的工作单元,用户只需决定事务开始或结束的界限。如果事务被放弃——也就是说,commit()没有被显式调用——那么事务的更改将被放弃,数据库保持不变。
事务可以被中止(使用Transaction.rollback()方法回滚),这样作为事务的一部分发生的任何更改都会被丢弃。这允许您保证数据模型的一致性。
例如,假设您正在创建一个订单输入系统,订单由一个Order对象、LineItem对象和一个Customer对象组成。如果您正在编写一个有七个行项目的订单,而第六个行项目由于无效数据而失败, 9 您不希望一个不完整的订单在数据库中徘徊。您可能希望回滚更改,并给用户一个机会用正确的数据再试一次。
当然,事务的定义也有例外,Hibernate 提供了多种类型的事务(例如,您可能有一个允许读取未提交数据的事务,即“脏读”)。此外,不同的数据库可能以自己的方式定义事务边界。幸运的是,这对于数据库来说是一个非常重要的问题,所以每个数据库都倾向于记录事务是如何定义的。10
排名的全面测试
我们已经看到了测试的许多部分,但是让我们把它们放在一起。等等,这不是一个简短的列表,但这是完整的类,包括用于打印的标签等等。
package chapter03.hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.query.Query;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.IntSummaryStatistics;
import java.util.stream.Collectors;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
public class RankingTest {
private SessionFactory factory;
@BeforeMethod
public void setup() {
StandardServiceRegistry registry =
new StandardServiceRegistryBuilder()
.configure()
.build();
factory = new MetadataSources(registry)
.buildMetadata()
.buildSessionFactory();
}
@AfterMethod
public void shutdown() {
factory.close();
}
//tag::testSaveRanking[]
@Test
public void testSaveRanking() {
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Person subject = savePerson(session, "J. C. Smell");
Person observer = savePerson(session, "Drew Lombardo");
Skill skill = saveSkill(session, "Java");
Ranking ranking = new Ranking();
ranking.setSubject(subject);
ranking.setObserver(observer);
ranking.setSkill(skill);
ranking.setRanking(8);
session.save(ranking);
tx.commit();
}
}
//end::testSaveRanking[]
//tag::testRankings[]
@Test
public void testRankings() {
populateRankingData();
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:name "
+ "and r.skill.name=:skill", Ranking.class);
query.setParameter("name", "J. C. Smell");
query.setParameter("skill", "Java");
IntSummaryStatistics stats = query.list()
.stream()
.collect(
Collectors.summarizingInt(Ranking::getRanking)
);
long count = stats.getCount();
int average = (int) stats.getAverage();
tx.commit();
session.close();
assertEquals(count, 3);
assertEquals(average, 7);
}
}
//end::testRankings[]
//tag::changeRanking[]
@Test
public void changeRanking() {
populateRankingData();
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:subject and "
+ "r.observer.name=:observer and "
+ "r.skill.name=:skill", Ranking.class);
query.setParameter("subject", "J. C. Smell");
query.setParameter("observer", "Gene Showrama");
query.setParameter("skill", "Java");
Ranking ranking = query.uniqueResult();
assertNotNull(ranking, "Could not find matching ranking");
ranking.setRanking(9);
tx.commit();
}
assertEquals(getAverage("J. C. Smell", "Java"), 8);
}
//end::changeRanking[]
//tag::removeRanking[]
@Test
public void removeRanking() {
populateRankingData();
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Ranking ranking = findRanking(session, "J. C. Smell",
"Gene Showrama", "Java");
assertNotNull(ranking, "Ranking not found");
session.delete(ranking);
tx.commit();
}
assertEquals(getAverage("J. C. Smell", "Java"), 7);
}
//end::removeRanking[]
//tag::getAverage[]
private int getAverage(String subject, String skill) {
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:name "
+ "and r.skill.name=:skill", Ranking.class);
query.setParameter("name", subject);
query.setParameter("skill", skill);
IntSummaryStatistics stats = query.list()
.stream()
.collect(
Collectors.summarizingInt(Ranking::getRanking)
);
int average = (int) stats.getAverage();
tx.commit();
return average;
}
}
//end::getAverage[]
//tag::populateRankingData[]
private void populateRankingData() {
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
createData(session, "J. C. Smell", "Gene Showrama", "Java", 6);
createData(session, "J. C. Smell", "Scottball Most", "Java", 7);
createData(session, "J. C. Smell", "Drew Lombardo", "Java", 8);
tx.commit();
}
}
private void createData(Session session,
String subjectName,
String observerName,
String skillName,
int rank) {
Person subject = savePerson(session, subjectName);
Person observer = savePerson(session, observerName);
Skill skill = saveSkill(session, skillName);
Ranking ranking = new Ranking();
ranking.setSubject(subject);
ranking.setObserver(observer);
ranking.setSkill(skill);
ranking.setRanking(rank);
session.save(ranking);
}
//end::populateRankingData[]
//tag::findPerson[]
private Person findPerson(Session session, String name) {
Query<Person> query = session.createQuery(
"from Person p where p.name=:name",
Person.class
);
query.setParameter("name", name);
Person person = query.uniqueResult();
return person;
}
//end::findPerson[]
private Skill findSkill(Session session, String name) {
Query<Skill> query = session.createQuery(
"from Skill s where s.name=:name",
Skill.class
);
query.setParameter("name", name);
Skill skill = query.uniqueResult();
return skill;
}
private Skill saveSkill(Session session, String skillName) {
Skill skill = findSkill(session, skillName);
if (skill == null) {
skill = new Skill();
skill.setName(skillName);
session.save(skill);
}
return skill;
}
//tag::savePerson[]
private Person savePerson(Session session, String name) {
Person person = findPerson(session, name);
if (person == null) {
person = new Person();
person.setName(name);
session.save(person);
}
return person;
}
//end::savePerson[]
//tag::findRanking[]
private Ranking findRanking(Session session,
String subject, String observer, String skill) {
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:subject and "
+ "r.observer.name=:observer and "
+ " r.skill.name=:skill", Ranking.class);
query.setParameter("subject", subject);
query.setParameter("observer", observer);
query.setParameter("skill", skill);
Ranking ranking = query.uniqueResult();
return ranking;
}
//end::findRanking[]
}
Listing 3-15src/test/java/chapter03/hibernate/RankingTest.java
编写我们的示例应用
到目前为止我们看到了什么?我们已经看到了以下内容:
-
对象模型的创建
-
对象模型到数据模型的映射,虽然不完整,但是很简单
-
将数据从对象模型写入数据库
-
将数据从数据库读入对象模型
-
通过我们的对象模型更新数据库中的数据
-
通过我们的对象模型从数据库中删除数据
有了所有这些,我们就可以开始设计我们的实际应用了,有了对象模型工作的知识(尽管还没有考虑效率)和示例代码来执行我们需求指定的大多数任务。
我们将像编写示例代码一样设计我们的应用;也就是说,我们将定义一个应用层(服务)并从测试中调用该应用。在现实世界中,我们将编写一个使用这些服务的用户界面层,就像测试一样。
澄清一下,我们的用户交互是
-
添加观察者对主题的排名。
-
由观察者更新主题的排名。
-
删除观察者对某一主题的排名。
-
查找某一科目特定技能的平均排名。
-
查找某个主题的所有排名。
-
找到某项技能排名最高的科目。
这听起来很多,但是我们已经写了大部分代码;我们只需要将其重构为一个服务层,以便于使用。
我们将把这些方法放到一个接口中,从清单 3-16 开始,但是在我们这样做之前,我们想抽象出一些基本的服务——主要是会话的获取。为此,我们将向父项目添加一个新模块——“util”模块——目前只有一个类,即SessionUtil。
在应用服务器(如 WildFly、GlassFish 或 Geronimo)中,通过资源注入访问持久化 API 应用部署者为 Java 持久化 API 配置一个上下文,应用自动获取一个EntityManager(相当于Session的 JPA)。将 Hibernate 配置为 JPA 提供者是完全可能的(也可能是更好的);然后,您可以使用 Hibernate APIs,并将其转换为Session。
您也可以通过 Spring 或 Guice 之类的库获得同样的资源注入。例如,使用 Spring,您可以配置一个持久化提供者,就像在 Java EE 应用服务器中一样,Spring 会自动提供一个资源,通过它您可以获取Session对象。
然而,尽管这些平台(Spring、Jakarta EE 和其他平台)都非常有用和实用(在 Jakarta EE 的情况下,可能是必要的),但我们将在很大程度上避免使用它们,因为我们希望限制我们对 Hibernate 所做工作的范围,而不是讨论各种竞争性的架构选择。
在源代码中,除了章节模块之外,还有一个“util”模块。com.autumncode.hibernate.util.SessionUtil类是一个单独的类,它提供了对SessionFactory的访问——到目前为止,我们已经把它放入了测试初始化代码中。它看起来就像你在清单 3-16 中看到的一样。
package com.autumncode.hibernate.util;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.function.Consumer;
import java.util.function.Function;
public class SessionUtil {
private static final SessionUtil instance = new SessionUtil();
private static final String CONFIG_NAME = "/configuration.properties";
private SessionFactory factory;
private Logger logger = LoggerFactory.getLogger(this.getClass());
private SessionUtil() {
initialize();
}
public static Session getSession() {
return getInstance().factory.openSession();
}
public static void forceReload() {
getInstance().initialize();
}
private static SessionUtil getInstance() {
return instance;
}
private void initialize() {
logger.info("reloading factory");
StandardServiceRegistry registry =
new StandardServiceRegistryBuilder()
.configure()
.build();
factory = new MetadataSources(registry)
.buildMetadata()
.buildSessionFactory();
}
}
Listing 3-16../util/src/main/java/com/autumncode/hibernate/util/SessionUtil.java
这个类有一个“forceReload()”方法(在初始化时使用),它为我们提供了一个简单的方法来重新加载一个新的数据库上下文。如果我们需要将数据库重置为一个已知的状态,我们可以调用这个方法并强制 Hibernate 重新初始化自己。我们将在本书的后面部分看到这一点(特别是在第十三章),我们将数据库设置为当我们使用它时自动删除;重新初始化意味着数据库将被删除,并在原始状态下重新创建。 11
SessionUtil的实际源代码有一些额外的方法。这就是为什么有这么多导入的类没有被使用,比如Consumer。我们将在本书的后面介绍它们。
我们实际上有一个非常非常简单的测试,断言它返回一个Session,如清单 3-17 所示。
package com.autumncode.hibernate.util;
import com.autumncode.util.model.Thing;
import org.hibernate.Session;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
public class SessionBuilderTest {
@Test
public void testSessionFactory() {
try (Session session = SessionUtil.getSession()) {
assertNotNull(session);
}
}
}
Listing 3-17../util/src/test/java/com/autumncode/hibernate/util/SessionBuilderTest.java
和SessionUtil一样,在实际的章节源代码中有额外的方法没有包括在这里;我们很快就会覆盖它们。
如你所见,SessionUtil类不做任何我们到目前为止还没有做过的事情;它只是在一个具有一般可见性的类中实现。我们可以将对该模块的依赖添加到其他项目中,并立即有一个干净的方法来获取会话——如果需要,我们可以通过 Jakarta EE 持久化机制,或通过 Spring, 12 使用该类作为获取会话的抽象。
想要更详细地查看util项目吗?我们将在第七章中介绍它。
添加排名
我们希望能够做的第一件事是添加一个排名。让我们首先通过创建我们的客户端代码来做这件事,这会给我们一个我们需要写什么的想法。参见清单 3-18 。
package chapter03.application;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
public class AddRankingTest {
RankingService service = new HibernateRankingService();
@Test
public void addRanking() {
service.addRanking("J. C. Smell", "Drew Lombardo", "Mule", 8);
assertEquals(service.getRankingFor("J. C. Smell", "Mule"), 8);
}
}
Listing 3-18src/test/java/chapter3/application/AddRankingTest.java
我们还没有编写接口或它的实现——我们将在下一个清单中纠正。在这里,我们只是在测试 API,看看它看起来怎么样,是否适合我们需要做的事情。
查看清单 3-19 中的代码,我们可以很容易地说addRanking()在逻辑上给 J. C. Smell 增加了一个等级,正如德鲁·伦巴多(Drew Lombardo)所观察到的,关于技能等级为 8 的骡子。很容易混淆参数。我们必须确保给它们起个清晰的名字,但是即使有了清晰的名字,也有可能产生混淆。
同样,我们可以说getRankingFor()相当清楚地检索了 J. C. Smell 在 Mule 的技能排名。再次,类型混淆的可能性潜伏着;如果我们调用getRankingFor("Mule", "J. C. Smell");,编译器将无法立即告诉我们,虽然我们可能能够在代码中减轻这种情况, 13 使用这种结构,总是会有混淆的可能性。
公平地说,API 的这一方面足够清晰,并且易于测试;让我们开始写一些代码。
清单 3-19 中显示的测试代码给出了 RankingService 的结构,至少有这两种方法。
package chapter03.application;
import chapter03.hibernate.Person;
import java.util.Map;
public interface RankingService {
int getRankingFor(String subject, String skill);
void addRanking(String subject, String observer, String skill, int ranking);
}
Listing 3-19src/main/java/chapter3/application/RankingService.java
示例代码中完整的RankingService有更多的方法。像往常一样,我们会看到他们,并根据需要添加他们,我们很快就会看到完整的班级。
现在让我们看看一些HibernateRankingService,它们将重用我们编写的大部分代码来测试我们的数据模型。
我们在这个类中所做的事情相当简单:我们有一个顶级方法(公开可见的方法)来获取一个会话,然后将该会话和其余的数据委托给一个 worker 方法。worker 方法处理数据操作,并且在很大程度上是来自RankingTest的createData()方法的副本,并且也使用我们为RankingTest编写的其他实用方法。
我们为什么要这么做?大多数情况下,我们预计其他方法可能需要使用addRanking()来参与现有的会话。参见清单 3-20 ,这只是另一个部分清单。
@Override
public void addRanking(String subjectName,
String observerName,
String skillName,
int rank) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
addRanking(session, subjectName, observerName,
skillName, rank);
tx.commit();
}
}
private void addRanking(Session session,
String subjectName,
String observerName,
String skillName,
int rank) {
Person subject = savePerson(session, subjectName);
Person observer = savePerson(session, observerName);
Skill skill = saveSkill(session, skillName);
Ranking ranking = new Ranking();
ranking.setSubject(subject);
ranking.setObserver(observer);
ranking.setSkill(skill);
ranking.setRanking(rank);
session.save(ranking);
}
Listing 3-20src/main/java/chapter3/application/HibernateRankingService.java
这使得我们的getRankingFor()方法没有实现;然而,正如addRanking()从RankingTest中被提升到接近完成,我们可以复制getAverage()的代码并改变Session的获取方式,如清单 3-21 所示。
@Override
public int getRankingFor(String subject, String skill) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
int average = getRankingFor(session, subject, skill);
tx.commit();
return average;
}
}
private int getRankingFor(Session session, String subject,
String skill) {
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:name "
+ "and r.skill.name=:skill", Ranking.class);
query.setParameter("name", subject);
query.setParameter("skill", skill);
IntSummaryStatistics stats = query
.list()
.stream()
.collect(
Collectors.summarizingInt(Ranking::getRanking)
);
return (int) stats.getAverage();
}
Listing 3-21src/main/java/chapter3/application/HibernateRankingService.java
就像使用addRanking()方法一样,publicly visible 方法分配一个Session,然后委托给一个内部方法,这是出于同样的原因:我们可能想要计算现有会话的平均值。(在下一节中,当我们想要更新一个Ranking时,我们将看到它的实际应用。)
声明一下,这个内部方法仍然很糟糕。它可以工作,但是我们可以对它进行一些优化。然而,我们的数据集如此之小,以至于没有意义。我们会到达那里的。
现在,当我们运行测试时(使用顶层目录中的mvn test,或者通过您的 IDE,如果您正在使用的话),AddRankingTest通过了,没有任何戏剧性的变化——这正是我们想要的。更令人满意的是,如果我们想摆弄一下HibernateRankingService的内部,我们可以;一旦有东西坏了,我们就能知道,因为我们的测试要求东西正常工作。
此外,如果你非常仔细地看——好吧,不是那么仔细,因为这是相当明显的——你会发现我们还设法满足了我们的另一个要求:确定给定科目技能的平均排名。尽管如此,我们还没有一个严格的测试。我们也会到达那里。
更新排名
接下来,我们处理(不太可能)更新一个Ranking的情况。这可能非常简单,但是我们需要考虑如果预先存在的Ranking不存在会发生什么。想象一下 Drew Lombardo 试图将 J. C. Smell 对 Mule 的掌握程度更改为 8,而他并没有为 J. C .和 Mule 提供任何优先排名。
我们可能不需要考虑太多,因为在这种情况下,我们很可能只是添加了Ranking,但其他更关键的应用可能需要额外的时间来思考。
实际上,让我们创建两个测试:一个使用现有的Ranking,另一个使用不存在的Ranking;见清单 3-22 。
package chapter03.application;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
public class UpdateRankingTest {
RankingService service = new HibernateRankingService();
static final String SCOTT = "Scotball Most";
static final String GENE = "Gene Showrama";
static final String CEYLON = "Ceylon";
@Test
public void updateExistingRanking() {
service.addRanking(GENE, SCOTT, CEYLON, 6);
assertEquals(service.getRankingFor(GENE, CEYLON), 6);
service.updateRanking(GENE, SCOTT, CEYLON, 7);
assertEquals(service.getRankingFor(GENE, CEYLON), 7);
}
@Test
public void updateNonexistentRanking() {
assertEquals(service.getRankingFor(SCOTT, CEYLON), 0);
service.updateRanking(SCOTT, GENE, CEYLON, 7);
assertEquals(service.getRankingFor(SCOTT, CEYLON), 7);
}
}
Listing 3-22src/test/java/chapter3/application/UpdateRankingTest.java
这两个测试非常简单。
updateExistingRanking()首先添加一个Ranking,然后检查它是否被正确添加;它更新相同的Ranking,然后确定平均值是否已经改变。由于这是该科目和该技能的唯一Ranking,所以平均值应该与变化后的Ranking相匹配。
updateNonExistentRanking()做几乎相同的事情:它确保我们没有这个主题和技能(即,它检查 0,我们的“不存在排名”的信号值),然后“更新”那个Ranking(根据我们的要求,它应该添加Ranking),然后检查结果平均值。
现在让我们看看用于实现这一点的服务代码,如清单 3-23 所示。 14
@Override
public void updateRanking(String subject,
String observer,
String skill,
int rank) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Ranking ranking = findRanking(session, subject,
observer, skill);
if (ranking == null) {
addRanking(session, subject, observer, skill, rank);
} else {
ranking.setRanking(rank);
}
tx.commit();
}
}
Listing 3-23src/main/java/chapter3/application/HibernateRankingService.java
值得考虑的是,这段代码的效率可能会更高。由于已经更改的记录中没有要保留的状态,所以我们可以可行地删除已经存在的记录,然后添加一个新记录。
然而,如果排名有某种时间戳——可能是createTimestamp和lastUpdatedTimestamp属性——那么在这种情况下,更新(如我们在这里所做的)更有意义。我们的数据模型不完整; 15 我们可以预料到在某个时候会添加这样的字段。
删除排名
移除一个Ranking需要考虑两个条件:一个是Ranking存在(当然!),另一个就是不存在。这可能是因为我们的架构需求要求移除一个不存在的Ranking是一个错误;但是在这种情况下,我们将假设删除只是试图验证Ranking不存在。
我们的测试代码如清单 3-24 所示。
package chapter03.application;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
public class RemoveRankingTest {
RankingService service = new HibernateRankingService();
@Test
public void removeRanking() {
service.addRanking("R1", "R2", "RS1", 8);
assertEquals(service.getRankingFor("R1", "RS1"), 8);
service.removeRanking("R1", "R2", "RS1");
assertEquals(service.getRankingFor("R1", "RS1"), 0);
}
@Test
public void removeNonexistentRanking() {
service.removeRanking("R3", "R4", "RS2");
}
}
Listing 3-24src/test/java/chapter3/application/RemoveRankingTest.java
这些测试应该很容易通过。
第一个测试(removeRanking())创建了一个Ranking,并验证它给出了一个已知的平均值,然后移除它,这应该会将平均值改回 0(这表明不存在那个Ranking的数据,如前所述)。
第二个测试调用了不应该存在的removeRanking()(因为我们没有在任何地方创建它);它应该不会改变这个主题。
值得指出的是,我们的测试已经相当完整了,但是还不够完整。例如,我们的一些测试可能会无意中向数据库添加数据,这取决于服务是如何编写的。虽然这对这个应用来说不是很重要,但是考虑一下如何在测试运行后验证整个数据库状态是值得的。
像 Spring Boot 和 Quarkus 这样的应用框架自动化了数据库初始化机制。还有一些库,比如 Flyway 和 Liquibase,可以根据需要为您填充数据库。
当然,我们需要removeRanking的代码。
@Override
public void removeRanking(String subject,
String observer,
String skill) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
removeRanking(session, subject, observer, skill);
tx.commit();
}
}
private void removeRanking(Session session,
String subject,
String observer,
String skill) {
Ranking ranking = findRanking(session, subject,
observer, skill);
if (ranking != null) {
session.delete(ranking);
}
}
Listing 3-25src/main/java/chapter3/application/HibernateRankingService.java
查找科目技能的平均排名
我们正在接近开始耗尽为测试我们的数据模型而编写的代码库的点。是时候验证为给定主题计算给定技能平均排名的代码了。我们已经使用这些代码来验证我们的其他需求(实际上,到目前为止所有的需求),但是我们使用的数据有限。让我们用更多的数据来验证getRankingFor()方法是否真的在做它应该做的事情。
我们的测试代码如清单 3-26 所示。
package chapter03.application;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
public class FindAverageRankingTest {
RankingService service = new HibernateRankingService();
@Test
public void validateRankingAverage() {
service.addRanking("A", "B", "C", 4);
service.addRanking("A", "B", "C", 5);
service.addRanking("A", "B", "C", 6);
assertEquals(service.getRankingFor("A", "C"), 5);
service.addRanking("A", "B", "C", 7);
service.addRanking("A", "B", "C", 8);
assertEquals(service.getRankingFor("A", "C"), 6);
}
}
Listing 3-26src/test/java/chapter3/application/FindAverageRankingTest.java
我们实际上对服务没有任何改变——它使用了我们已经在“添加排名”一节中看到的getRankingFor()方法。
查找某个主题的所有排名
我们在这里寻找的是给定主题的技能列表及其平均值。
关于如何表示这些数据,我们有几种选择;我们是否想要一个Map<String, Integer>,这样我们就可以很容易的定位一个技能对应什么技能等级?我们要不要一个队列,让技能等级按顺序排列?
这将取决于交互的架构需求。在这个级别(对于这个特殊的应用设计),我们将使用一个Map<String, Integer>;它以简单的数据结构为我们提供了所需的数据(一组技能及其平均排名)。最终,我们可能会重新审视这个需求,并更有效地满足它。
像往常一样,让我们编写测试代码,然后让它正常运行;参见清单 3-27 。
package chapter03.application;
import org.testng.annotations.Test;
import java.util.Map;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
public class FindAllRankingsTest {
RankingService service = new HibernateRankingService();
@Test
public void findAllRankingsEmptySet() {
assertEquals(service.getRankingFor("Nobody", "Java"), 0);
assertEquals(service.getRankingFor("Nobody", "Python"), 0);
Map<String, Integer> rankings = service.findRankingsFor("Nobody");
// make sure our dataset size is what we expect: empty
assertEquals(rankings.size(), 0);
}
@Test
public void findAllRankings() {
assertEquals(service.getRankingFor("Somebody", "Java"), 0);
assertEquals(service.getRankingFor("Somebody", "Python"), 0);
service.addRanking("Somebody", "Nobody", "Java", 9);
service.addRanking("Somebody", "Nobody", "Java", 7);
service.addRanking("Somebody", "Nobody", "Python", 7);
service.addRanking("Somebody", "Nobody", "Python", 5);
Map<String, Integer> rankings = service.findRankingsFor("Somebody");
assertEquals(rankings.size(), 2);
assertNotNull(rankings.get("Java"));
assertEquals(rankings.get("Java"), new Integer(8));
assertNotNull(rankings.get("Python"));
assertEquals(rankings.get("Python"), new Integer(6));
}
}
Listing 3-27src/test/java/chapter3/application/FindAllRankingsTest.java
当然,我们在这里有两个测试:第一个测试寻找一个没有数据的主题,它验证我们得到了一个空的数据集。
第二个验证我们没有主题的数据,填充一些数据,然后寻找排名平均值集。然后,它确保我们有我们期望的平均数,并验证排名本身就是我们期望的。
同样,也许编写更完整的测试是可行的,但是这些测试确实验证了我们的简单需求是否得到满足。我们仍然没有检查副作用,但这超出了本章的范围。 16
所以我们来看看findRankingsFor()的代码。像往常一样,我们将有一个公共方法,然后是一个参与现有Session的内部方法,如清单 3-28 所示。
@Override
public Map<String, Integer> findRankingsFor(String subject) {
Map<String, Integer> results;
try (Session session = SessionUtil.getSession()) {
return findRankingsFor(session, subject);
}
}
private Map<String, Integer> findRankingsFor(Session session,
String subject) {
Map<String, Integer> results = new HashMap<>();
Query<Ranking> query = session.createQuery(
"from Ranking r where "
+ "r.subject.name=:subject order by r.skill.name",
Ranking.class);
query.setParameter("subject", subject);
List<Ranking> rankings = query.list();
String lastSkillName = "";
int sum = 0;
int count = 0;
for (Ranking r : rankings) {
if (!lastSkillName.equals(r.getSkill().getName())) {
sum = 0;
count = 0;
lastSkillName = r.getSkill().getName();
}
sum += r.getRanking();
count++;
results.put(lastSkillName, sum / count);
}
return results;
}
Listing 3-28src/main/java/chapter3/application/HibernateRankingService.java
内部findRankingsFor()方法(和我们所有计算平均值的方法一样)真的不是很有吸引力。当我们遍历排名时,它使用控制中断机制来计算平均值。 17
根据维基百科关于控制中断( https://en.wikipedia.org/wiki/Control_break )的页面,“使用 SQL 等第四代语言,编程语言应该自动处理控制中断的大部分细节。”这是绝对正确的,这也是为什么我一直指出所有这些程序的低效率。我们正在手动做一些数据库(和 Hibernate)应该能够为我们做的事情——它确实能够做到。我们只是还没有使用这种能力。当我们查看下一个应用需求时,我们将最终实现这一点。
可以使用 Java 中的 Streams API 将排名列表转换为技能和技能平均值的地图。然而,它几乎和这里使用的控制中断一样不自然,对大多数人来说更难阅读。最后,因为平均值无论如何都应该由数据库来计算,所以使用 Streams API 来完成这个任务是多余的。
无论如何,新的测试应该能够通过(也许不能顺利通过,因为实际的底层服务没有考虑效率),这允许我们转移到最后一个(也可能是最复杂的)需求。
找到技能排名最高的科目
有了这个需求,我们想要找出对于一个给定的技能谁排名最高;如果我们有三个人的 Java 排名,我们希望他们的平均分是最好的。如果这项技能没有排名,我们需要一个空响应作为信号值。去参加考试;让我们看看清单 3-29 。
package chapter03.application;
import chapter03.hibernate.Person;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
public class FindBestRankingTest {
RankingService service = new HibernateRankingService();
@Test
public void findBestForNonexistentSkill() {
Person p = service.findBestPersonFor("no skill");
assertNull(p);
}
@Test
public void findBestForSkill() {
service.addRanking("S1", "O1", "Sk1", 6);
service.addRanking("S1", "O2", "Sk1", 8);
service.addRanking("S2", "O1", "Sk1", 5);
service.addRanking("S2", "O2", "Sk1", 7);
service.addRanking("S3", "O1", "Sk1", 7);
service.addRanking("S3", "O2", "Sk1", 9);
// data that should not factor in!
service.addRanking("S1", "O2", "Sk2", 2);
Person p = service.findBestPersonFor("Sk1");
assertEquals(p.getName(), "S3");
}
}
Listing 3-29src/test/java/chapter3/application/FindBestRankingTest.java
我们的第一个测试应该是显而易见的:给定一个不存在的技能,我们不应该得到一个Person back。(这符合我们建议使用信号值而不是异常的既定惯例。)
我们的第二个测试创建了三个主题,每个主题都具有“Sk1”中的技能,不管那是什么。(是“作为测试数据的能力。”)S1 平均为 7,S2 平均为 6,S3 平均为 8。因此,我们应该期待 S3 成为最佳排名的拥有者。我们加入了一些离群数据,只是为了确保我们的服务仅限于它试图找到的实际数据。
请注意,我们实际上并没有返回技能的平均值!对于实际的应用,这很可能是一个需求;这很容易通过立即调用getRankingFor()来实现,但是考虑到目前为止该方法的设计,这是一个非常昂贵的操作(涉及一系列数据库往返)。不久我们将再次讨论这个问题;这里,我们使用尽可能少的对象类型。
所以让我们看看清单 3-30 中的一些代码。我们最终将进入一个更强大的查询(看看我们如何更有效地编写一些其他的查询)。
@Override
public Person findBestPersonFor(String skill) {
Person person = null;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
person = findBestPersonFor(session, skill);
tx.commit();
}
return person;
}
private Person findBestPersonFor(Session session, String skill) {
Query<Object[]> query = session.createQuery(
"select r.subject.name, avg(r.ranking)"
+ " from Ranking r where "
+ "r.skill.name=:skill "
+ "group by r.subject.name "
+ "order by avg(r.ranking) desc", Object[].class);
query.setParameter("skill", skill);
query.setMaxResults(1);
List<Object[]> result = query.list();
if (result.size() > 0) {
// we want the first (and only) row
Object[] row=result.get(0);
String personName=row[0].toString();
return findPerson(session, personName);
}
return null;
}
Listing 3-30src/main/java/chapter3/application/HibernateRankingService.java
我们的公共方法遵循我们到目前为止已经建立的约定:创建一个会话,然后委托给一个内部方法。
不过,内部方法做了一些我们到目前为止还没有见过的事情,从一种不同类型的查询开始。
我们的大多数查询都是“FROM class alias WHERE condition”形式,这相当简单。Hibernate 正在生成使用表名的 SQL,并且可以自动执行连接来遍历数据树(例如“r.skillname”),但是总体形式非常简单。
这里,我们有一个实际的SELECT子句。下面是用代码编写的完整查询:
select r.subject.name, avg(r.ranking)
from Ranking r
where r.skill.name=:skill
group by r.subject.name
order by avg(r.ranking)
desc
这实际上返回元组,元组是对象数组的集合。(这被称为“投影”,我们实际上可以创建一个表示投影的类,但是我们将在后面的章节中演示。)
我们的“select”子句指定元组将有两个值:从与排名相关的主题名称中提取的值和一个计算值,在这种情况下,该计算值是特定组中所有排名的平均值。
“where”子句将整个数据集限制为技能名称与参数匹配的那些排名。
“group by”子句意味着值集被一起处理,这又意味着平均排名(查询返回的元组的第二个值)将限于每个主题。
“order by”子句意味着 Hibernate 将在排序较低的主题之前给我们排序最高的主题。
我们还将查询的最大结果数设置为…1,因为我们只对给定技能排名最高的人感兴趣;如果一项技能很受欢迎(比如“Java”),可能会有成千上万的排名,而我们正在寻找最佳人选——所以我们只希望返回一个元素。
实际上,在这里设置最大行数的应用是有限的;该查询返回字节流,不会在第一次请求时传输整个数据集;它会根据需要检索信息块,所以即使查询结果有很多兆字节,它也只会提供我们使用的信息。但是,使用最大行数也可以告诉数据库可能的优化参数。
我们能以编程的方式完成所有这些吗?当然,我们可以;这就是我们在大部分代码中看到的,我们手动计算平均技能值。然而,这节省了往返数据时间;数据库实际上执行计算并返回一个足够大的数据集来完成它的任务。在这种计算中,数据库通常会针对效率进行调整,所以如果我们不像本例中那样使用嵌入式数据库,我们可能也会节省时间。 18
把这一切放在一起
如前所述,这是本章的RankingService和HibernateRankingService的完整列表。//tag::和//end::的评论是为了发表;当本章被呈现时,它使用实际的示例源,而不是从源复制到章节中。(因此,如果作者注意到了什么,并在“活动源代码”中修复了它,则该章会自动更正。)
package chapter03.application;
import chapter03.hibernate.Person;
import chapter03.hibernate.Ranking;
import chapter03.hibernate.Skill;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import java.util.HashMap;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class HibernateRankingService implements RankingService {
//tag::getRankingFor[]
@Override
public int getRankingFor(String subject, String skill) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
int average = getRankingFor(session, subject, skill);
tx.commit();
return average;
}
}
private int getRankingFor(Session session, String subject,
String skill) {
Query<Ranking> query = session.createQuery(
"from Ranking r "
+ "where r.subject.name=:name "
+ "and r.skill.name=:skill", Ranking.class);
query.setParameter("name", subject);
query.setParameter("skill", skill);
IntSummaryStatistics stats = query
.list()
.stream()
.collect(
Collectors.summarizingInt(Ranking::getRanking)
);
return (int) stats.getAverage();
}
//end::getRankingFor[]
//tag::addRanking[]
@Override
public void addRanking(String subjectName,
String observerName,
String skillName,
int rank) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
addRanking(session, subjectName, observerName,
skillName, rank);
tx.commit();
}
}
private void addRanking(Session session,
String subjectName,
String observerName,
String skillName,
int rank) {
Person subject = savePerson(session, subjectName);
Person observer = savePerson(session, observerName);
Skill skill = saveSkill(session, skillName);
Ranking ranking = new Ranking();
ranking.setSubject(subject);
ranking.setObserver(observer);
ranking.setSkill(skill);
ranking.setRanking(rank);
session.save(ranking);
}
//end::addRanking[]
//tag::updateRanking[]
@Override
public void updateRanking(String subject,
String observer,
String skill,
int rank) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Ranking ranking = findRanking(session, subject,
observer, skill);
if (ranking == null) {
addRanking(session, subject, observer, skill, rank);
} else {
ranking.setRanking(rank);
}
tx.commit();
}
}
//end::updateRanking[]
//tag::removeRanking[]
@Override
public void removeRanking(String subject,
String observer,
String skill) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
removeRanking(session, subject, observer, skill);
tx.commit();
}
}
private void removeRanking(Session session,
String subject,
String observer,
String skill) {
Ranking ranking = findRanking(session, subject,
observer, skill);
if (ranking != null) {
session.delete(ranking);
}
}
//end::removeRanking[]
//tag::findRankingsFor[]
@Override
public Map<String, Integer> findRankingsFor(String subject) {
Map<String, Integer> results;
try (Session session = SessionUtil.getSession()) {
return findRankingsFor(session, subject);
}
}
private Map<String, Integer> findRankingsFor(Session session,
String subject) {
Map<String, Integer> results = new HashMap<>();
Query<Ranking> query = session.createQuery(
"from Ranking r where "
+ "r.subject.name=:subject order by r.skill.name",
Ranking.class);
query.setParameter("subject", subject);
List<Ranking> rankings = query.list();
String lastSkillName = "";
int sum = 0;
int count = 0;
for (Ranking r : rankings) {
if (!lastSkillName.equals(r.getSkill().getName())) {
sum = 0;
count = 0;
lastSkillName = r.getSkill().getName();
}
sum += r.getRanking();
count++;
results.put(lastSkillName, sum / count);
}
return results;
}
//end::findRankingsFor[]
//tag::findBestPersonFor[]
@Override
public Person findBestPersonFor(String skill) {
Person person = null;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
person = findBestPersonFor(session, skill);
tx.commit();
}
return person;
}
private Person findBestPersonFor(Session session, String skill) {
Query<Object[]> query = session.createQuery(
"select r.subject.name, avg(r.ranking)"
+ " from Ranking r where "
+ "r.skill.name=:skill "
+ "group by r.subject.name "
+ "order by avg(r.ranking) desc", Object[].class);
query.setParameter("skill", skill);
query.setMaxResults(1);
List<Object[]> result = query.list();
if (result.size() > 0) {
// we want the first (and only) row
Object[] row=result.get(0);
String personName=row[0].toString();
return findPerson(session, personName);
}
return null;
}
//end::findBestPersonFor[]
private Ranking findRanking(Session session, String subject,
String observer, String skill) {
Query<Ranking> query = session.createQuery(
"from Ranking r where "
+ "r.subject.name=:subject and "
+ "r.observer.name=:observer and "
+ "r.skill.name=:skill", Ranking.class);
query.setParameter("subject", subject);
query.setParameter("observer", observer);
query.setParameter("skill", skill);
Ranking ranking = query.uniqueResult();
return ranking;
}
private Person findPerson(Session session, String name) {
Query<Person> query = session.createQuery(
"from Person p where p.name=:name",
Person.class);
query.setParameter("name", name);
Person person = query.uniqueResult();
return person;
}
private Skill findSkill(Session session, String name) {
Query<Skill> query = session.createQuery(
"from Skill s where s.name=:name", Skill.class);
query.setParameter("name", name);
Skill skill = query.uniqueResult();
return skill;
}
private Skill saveSkill(Session session, String skillName) {
Skill skill = findSkill(session, skillName);
if (skill == null) {
skill = new Skill();
skill.setName(skillName);
session.save(skill);
}
return skill;
}
private Person savePerson(Session session, String name) {
Person person = findPerson(session, name);
if (person == null) {
person = new Person();
person.setName(name);
session.save(person);
}
return person;
}
}
Listing 3-32src/main/java/chapter3/application/HibernateRankingService.java
//tag::preamble[]
package chapter03.application;
import chapter03.hibernate.Person;
import java.util.Map;
public interface RankingService {
int getRankingFor(String subject, String skill);
void addRanking(String subject, String observer, String skill, int ranking);
//end::preamble[]
void updateRanking(String subject, String observer, String skill, int ranking);
void removeRanking(String subject, String observer, String skill);
Map<String, Integer> findRankingsFor(String subject);
Person findBestPersonFor(String skill);
}
Listing 3-31src/main/java/chapter3/application/RankingService.java
摘要
在这一章中,我们已经看到了如何从问题定义到对象模型,以及一个测试驱动设计来测试模型的例子。我们还略微涉及了与持久化和事务相关的对象状态的概念。
然后,我们将重点放在应用需求上,构建一系列可以满足这些需求的操作。我们讲述了如何创建、读取、更新和删除数据,以及如何使用 Hibernate 的查询语言对计算数据执行相当复杂的查询。
在下一章,我们将看看 Hibernate 的架构和基于 Hibernate 的应用的生命周期。
四、持久化生命周期
在本章中,我们将讨论 Hibernate 中持久对象的生命周期。这些持久对象可以是 POJOs,不需要任何特殊的标记接口或与 Hibernate 相关的继承。Hibernate 受欢迎的部分原因是它能够使用普通的对象模型。
我们还将介绍一些用于从 Hibernate 创建、检索、更新和删除持久对象的Session接口的方法。
生命周期介绍
将 Hibernate 添加到应用之后,您不需要更改现有的 Java 对象模型来添加持久标记接口或任何其他类型的 Hibernate 提示。相反,Hibernate 处理应用用new操作符创建的普通 Java 对象或其他对象创建的普通 Java 对象。
出于 Hibernate 的目的,这些可以分为两类:Hibernate 有实体映射的对象和 Hibernate 不能直接识别的对象。正确映射的实体对象将由被映射的字段和属性组成,这些字段和属性本身或者是对正确映射的实体的引用,或者是对这些实体的集合的引用,或者是“值”类型(原语、原语包装、字符串或它们的数组)。
给定一个映射到 Hibernate 的对象实例,它可以处于四种不同状态中的任何一种:暂时、持久、分离或删除。 1
短暂的对象只存在于内存中。Hibernate 不管理瞬态对象,也不保存对瞬态对象的更改。如果你有一个Person POJO,并且你用new Person()创建了一个实例,那么这个对象就是瞬态,并且只要它处于瞬态状态,就不期望它以某种方式在数据库中被表示出来。
要持久化对瞬态对象的更改,您必须请求会话将瞬态对象保存到数据库,这时 Hibernate 会给对象分配一个标识符,并将对象标记为处于持久状态。
持久对象存在于数据库中,Hibernate 管理持久对象的持久化。我们在图 4-1 中展示了对象和数据库之间的关系。如果持久化对象的字段或属性发生了变化,Hibernate 会在应用将这些变化标记为已提交时保持数据库表示的最新状态。

图 4-1
持久对象 由休眠维护
分离的对象在数据库中有表示,但对对象的更改不会反映在数据库中,反之亦然。对象和数据库的暂时分离如图 4-2 所示。分离的对象可以通过关闭与它相关联的会话来创建,或者通过调用会话的evict()方法将其从会话中逐出来创建。
考虑分离实体的一个原因是从数据库中读取一个对象,在内存中修改该对象的属性,然后将结果存储在数据库之外的某个地方。这将是对对象进行深层复制的一种替代方法。

图 4-2
分离的对象存在于数据库中,不受 Hibernate 维护
为了持久保存对分离对象所做的更改,应用必须将其重新附加到有效的 Hibernate 会话。当您的应用调用新会话上的load()、refresh()、merge()、update()或save()方法之一并引用分离的对象时,分离的实例可以与新的 Hibernate 会话相关联。调用后,分离的对象将是由新的 Hibernate 会话管理的持久对象。
移除的对象是由 Hibernate 管理的对象(换句话说,是持久对象),它们已经被传递给会话的remove()方法。当应用将会话中保存的更改标记为已提交时,数据库中对应于已删除对象的条目将被删除。
Hibernate 3 之前的版本支持生命周期和可验证的接口。这些允许您的对象使用对象上的方法监听保存、更新、删除、加载和验证事件。在 Hibernate 3 中,这个函数移到了事件和拦截器中,旧的接口被移除了。从 Hibernate 4 开始,还支持 JPA 持久化生命周期,因此可以将事件嵌入到对象中,并用注释进行标记。
实体、类和名称
实体用映射表示 Java 对象,映射允许它们存储在数据库中。映射指示对象的字段和属性应该如何存储在数据库表中。但是,您可能希望特定类型的对象在数据库中以两种不同的方式表示。例如,您可以为用户创建一个 Java 类,但是在数据库中有两个不同的表来存储用户。这可能不是最好的数据库设计,但类似的问题在遗留系统中很常见。其他不容易修改的系统可能依赖于现有的数据库设计,Hibernate 足够强大,可以覆盖这种场景。在这种情况下,Hibernate 如何选择使用哪个?
代表实体的对象将是一个普通的 Java 类。它还将有一个实体名称。默认情况下,实体的名称将与类类型的名称相同。 2 但是,您可以选择通过映射或注释来改变这一点,从而区分映射到不同表的相同类型的对象。因此,Session API 中有一些方法需要提供一个实体名称来确定适当的映射。如果省略了这一点,要么是因为不需要这种区分,要么是因为为了方便起见,该方法假设了最常见的情况——实体名称与类名相同——并复制了另一个更具体的方法的功能,该方法允许显式指定实体名称。
标识符
标识符或标识列映射到关系数据库中主键的概念。主键是一个或多个列的唯一集合,可用于指定特定的数据集合。
有两种标识符:自然的和人工的。
一个自然标识符是应用认为有意义的东西——例如,一个用户 ID,或者一个社会安全号码 3 或者等同物。
人工标识符的值是任意的。到目前为止,我们的代码使用数据库生成的值(标识列),这些值与该标识符关联的数据没有任何关系。这倾向于在关联和其他这样的交互方面产生更大的灵活性,因为在许多情况下,人工标识符可以比自然标识符小。
为什么人工标识符会比自然标识符更好?嗯,有几个可能的原因。人工标识符可能比自然标识符更好的一个原因是,人工标识符可能是比自然标识符更小的类型(在内存中)。
考虑一封用户电子邮件。在大多数情况下,用户的电子邮件地址不会改变,对于给定的用户来说,它们往往是唯一的;然而,电子邮件地址可能至少有 20 个字节长 4 (也可能更长)。整数用户 ID(long 或 int)的长度可能是 4 或 8 个字节,不会更长。
另一个更有说服力的原因是,人工标识符不会随着数据的自然生命周期而改变。例如,电子邮件地址可能会随着时间而改变;有人可能会放弃旧的电子邮件地址,而选择一个新的。任何依赖该电子邮件地址作为自然标识符的东西都必须被同步改变以允许更新。
还有一个原因是人工标识符很简单。数据库(和 Hibernate)允许使用复合标识符——由一个对象的多个属性构建的标识符。然而,这意味着当您引用数据库中的一个特定对象或行时,您必须将所有列包含在标识符中,无论是作为嵌入对象还是作为一组单独的列。这当然是可行的;一些数据模型需要它(例如,由于遗留或其他业务原因)。然而,出于效率的考虑,大多数人通常更喜欢人工密钥。
在 Hibernate 中,对象属性用@Id 注释标记为标识符,如清单 4-1 所示。
@Id
public Long id;
Listing 4-1A Typical Identifier Field
在清单 4-1 中,您会看到一个Long——H2 的“大整数”——它被标记为一个大概的人工标识符。需要先分配该值,然后才能持久保存具有该属性的对象。
不过,在我们目前的示例代码中,我们没有手动分配标识符。我们使用了另一个注释@GeneratedValue,它告诉 Hibernate 它负责分配和维护标识符。发生这种情况的机制很大程度上取决于 Hibernate 配置和使用的数据库。
你可能已经错过了,但是@Id并不意味着一个标识符被自动分配。如果您不想自己分配标识符值,您必须使用一个@GeneratedValue注释!
有五种不同的生成可能性:标识、序列、表、自动和无。身份生成依赖于自然的表排序。这是通过使用GenerationType.IDENTITY选项在@GeneratedValue注释中请求的,如清单 4-2 所示。
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
public Long id;
Listing 4-2An Autogenerated Identity Field
身份生成非常方便,使用起来感觉非常自然。但是,在知道标识符之前,它需要将数据实际插入数据库,并且使用IDENTITY禁用插入的 JDBC 批处理(同样,因为在插入行的之后检测到标识符)。Hibernate 文档建议使用其他的生成策略,这一点毋庸置疑。
序列机制依赖于数据库创建表序列的能力(这倾向于将其限制在 PostgreSQL、Oracle 和其他一些数据库)。对应的是GenerationType.SEQUENCE策略。
表机制使用一个表,其目的是存储人工标识符块;您可以让 Hibernate 为您生成,或者您可以用一个附加的@TableGenerator 注释来指定表的所有细节。要通过表使用人工密钥生成,请使用GenerationType.TABLE策略。
第四种人工密钥生成策略是自动,它通常映射到IDENTITY策略,但是依赖于所讨论的数据库。(它应该默认为对所讨论的数据库有效的东西。)要使用这个,使用GenerationType.AUTO策略。
第五种策略实际上根本不是策略:它依赖于手动分配标识符。如果用一个空的标识符调用Session.persist(),就会抛出一个IdentifierGenerationException。
实体和协会
实体可以包含对其他实体的引用,可以直接作为嵌入的属性或字段,也可以通过某种集合(数组、集合、列表等)间接引用。).这些关联使用基础表中的外键关系来表示。这些外键将依赖于参与表所使用的标识符,这是更喜欢小(和人工)键的另一个原因。
当实体对中只有一个实体包含对另一个实体的引用时,关联是单向的。如果关联是相互的,那么它被称为是双向的。
设计实体模型时的一个常见错误是试图使所有关联都是双向的。不属于对象模型自然组成部分的关联不应该被强制加入其中。Hibernate 查询语言通常提供了一种更自然的方式来访问相同的信息。
在关联中,其中一个(且只有一个)参与类被称为“管理关系”如果关联的两端都管理关系,那么当客户端代码在关联的两端都调用适当的 set 方法时,我们就会遇到问题。应该维护两个外键列——每个方向一个(存在循环依赖的风险)——还是只维护一个?
理想情况下,我们希望规定只有对关系一端的更改才会导致对外键的任何更新;事实上,Hibernate 允许我们通过将关联的一端标记为由另一端管理(由关联注释的mappedBy属性标记)来做到这一点。
mappedBy纯粹是关于如何保存实体之间的外键关系。这与拯救实体本身无关。尽管如此,它们经常与完全正交的级联功能相混淆(在本章的“级联操作”一节中描述)。
虽然 Hibernate 允许我们指定对一个关联的更改将导致对数据库的更改,但是它不允许我们将对关联一端的更改自动反映到 Java POJOs 的另一端。
让我们来看看一些代码。这是第四章 ?? 的pom.xml,来自这本书的源代码;这不是特别有启发性,因为它很大程度上是其他章节的项目模型的直接复制,但它是很好的彻底。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hibernate-6-parent</artifactId>
<groupId>com.autumncode.books.hibernate</groupId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter04</artifactId>
<dependencies>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>util</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
Listing 4-3pom.xml
让我们在chapter04.broken包中创建一个例子,一个Message和一个Email关联,没有“拥有对象”首先是Message类,如清单 4-4 所示。
package chapter04.broken;
import javax.persistence.*;
@Entity
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column
String content;
@OneToOne
Email email;
public Message() {
}
public Message(String content) {
setContent(content);
}
// accessors and mutators ignored for brevity
@Override
public String toString() {
// note use of email.subject because otherwise properly constructed
// relationships would cause an endless loop that never ends
// and therefore runs endlessly.
return String.format(
"Message{id=%d, content='%s', email.subject='%s'}",
id,
content,
(email != null ? email.getSubject() : "null")
);
}
}
Listing 4-4A Broken Model, Beginning with Message
清单 4-5 是Email级。
package chapter04.broken;
import javax.persistence.*;
@Entity
public class Email {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column
String subject;
@OneToOne
// (mappedBy = "email")
Message message;
public Email() {
}
public Email(String subject) {
setSubject(subject);
}
// accessors and mutators ignored for brevity
@Override
public String toString() {
// note use of message.content because otherwise properly constructed
// relationships would cause an endless loop that never ends
// and therefore runs endlessly.
return String.format(
"Email{id=%s, subject=`%s`, message.content=%s}",
id,
subject,
(message != null ? message.getContent() : "null")
);
}
}
Listing 4-5A Broken Model’s Email Class
对于这些类,没有“拥有关系”;电子邮件中的mappedBy属性被注释掉。这意味着我们需要更新电子邮件和信息,以使我们的关系在两个方向上正确建模。先看完整的chapter04.broken.BrokenInversionTest类,看到源码后再分解。
package chapter04.broken;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
public class BrokenInversionTest {
@Test()
public void testBrokenInversionCode() {
Long emailId;
Long messageId;
Email email;
Message message;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
email = new Email("Broken");
message = new Message("Broken");
email.setMessage(message);
// message.setEmail(email);
session.save(email);
session.save(message);
emailId = email.getId();
messageId = message.getId();
tx.commit();
}
assertNotNull(email.getMessage());
assertNull(message.getEmail());
try (Session session = SessionUtil.getSession()) {
email = session.get(Email.class, emailId);
System.out.println(email);
message = session.get(Message.class, messageId);
System.out.println(message);
}
assertNotNull(email.getMessage());
assertNull(message.getEmail());
}
}
Listing 4-6src/main/java/chapter04/broken/BrokenInversionTest.java
对message.getEmail()的最后一次调用将返回null(假设使用了简单的访问器和赋值器)。为了获得想要的效果,两个实体都必须更新。如果Email实体拥有关联,这仅仅确保外键列值的正确赋值。没有message.setEmail(email)的没有的隐式调用。这必须显式给出,如清单 4-7 所示。
package chapter04.broken;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
import static org.testng.Assert.assertNotNull;
public class ProperSimpleInversionTest {
@Test
public void testProperSimpleInversionCode() {
Long emailId;
Long messageId;
Email email;
Message message;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
email = new Email("Proper");
message = new Message("Proper");
email.setMessage(message);
message.setEmail(email);
session.save(email);
session.save(message);
emailId = email.getId();
messageId = message.getId();
tx.commit();
}
assertNotNull(email.getMessage());
assertNotNull(message.getEmail());
try (Session session = SessionUtil.getSession()) {
email = session.get(Email.class, emailId);
System.out.println(email);
message = session.get(Message.class, messageId);
System.out.println(message);
}
assertNotNull(email.getMessage());
assertNotNull(message.getEmail());
}
}
Listing 4-7src/main/java/chapter04/broken/ProperSimpleInversionTest.java
最后一个断言是assertNotNull(),在BrokenInversionTest中是assertNull()。
对于刚接触 Hibernate 的用户来说,对这一点感到困惑是很常见的。发生这种情况的原因是 Hibernate 正在使用实体的实际当前状态。在BrokenInversionTest.java中,当您在电子邮件中设置消息,而不是在消息中设置电子邮件时,Hibernate 在对象模型中保存实际的关系,而不是试图推断一个关系,即使该关系是预期的。额外的关系将是一个意想不到的副作用,即使它在这种特殊情况下可能是有用的。
如果我们包括映射(mappedBy 属性),我们会得到不同的结果。我们将修改Message(通过将它移动到一个新的包中,chapter04.mapped)和Email(通过移动它并包含前面清单中注释掉的mappedBy属性)。
除了包和实体名(这意味着 Hibernate 将使用“Message2”作为该类型的表名),代码与“破损”版本相同,如清单 4-8 所示。
package chapter04.mapped;
import javax.persistence.*;
@Entity(name = "Message2")
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column
String content;
@OneToOne
Email email;
public Message() {
}
public Message(String content) {
setContent(content);
}
// accessors and mutators omitted
@Override
public String toString() {
// note use of email.subject because otherwise properly constructed
// relationships would cause an endless loop that never ends
// and therefore runs endlessly.
return String.format(
"Message{id=%d, content='%s', email.subject='%s'}",
id,
content,
(email != null ? email.getSubject() : "null")
);
}
}
Listing 4-8src/main/java/chapter04/mapped/Message.java
Email代码除了更改实体名称和包之外,还添加了mappedBy属性。这实际上给Message数据库表示添加了一列,表示电子邮件 ID。参见清单 4-9 。
package chapter04.mapped;
import javax.persistence.*;
@Entity(name = "Email2")
public class Email {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column
String subject;
@OneToOne(mappedBy = "email")
Message message;
public Email() {
}
public Email(String subject) {
setSubject(subject);
}
// accessors and mutators omitted
@Override
public String toString() {
// note use of message.content because otherwise properly constructed
// relationships would cause an endless loop that never ends
// and therefore runs endlessly.
return String.format(
"Email{id=%s, subject=`%s`, message.content=%s}",
id,
subject,
(message != null ? message.getContent() : "null")
);
}
}
Listing 4-9src/main/java/chapter04/mapped/Email.java
使用Message中包含的映射,会有一些意想不到的结果。我们之前的测试无法重新建立一些关系,需要在Email和Message中设置它们。这里,我们有几乎相同的构造,但是没有相同的结果:我们只需要设置关系的一边,而不是手动维护两个引用。
首先我们来看测试代码,如清单 4-10 所示;注意,这个测试使用了chapter04.mapped包,所以它得到了我们刚刚看到的Email和Message类。
package chapter04.mapped;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
import static org.testng.Assert.assertNotNull;
public class ImplicitRelationshipTest {
@Test
public void testImpliedRelationship() {
Long emailId;
Long messageId;
Email email;
Message message;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
email = new Email("Inverse Email");
message = new Message("Inverse Message");
// email.setMessage(message);
message.setEmail(email);
session.save(email);
session.save(message);
emailId = email.getId();
messageId = message.getId();
tx.commit();
}
assertEquals(email.getSubject(), "Inverse Email");
assertEquals(message.getContent(), "Inverse Message");
assertNull(email.getMessage());
assertNotNull(message.getEmail());
try (Session session = SessionUtil.getSession()) {
email = session.get(Email.class, emailId);
System.out.println(email);
message = session.get(Message.class, messageId);
System.out.println(message);
}
assertNotNull(email.getMessage());
assertNotNull(message.getEmail());
}
}
Listing 4-10src/test/java/chapter04/mapped/ImplicitRelationshipTest.java
这个测试通过了,尽管我们没有设置Email's Message。
那个mappingBy属性就是原因。在数据库中,Message2表将有一个名为“email_id”的列,当我们更新Message's email属性时,它被设置为Email's的唯一标识符。当我们关闭会话并重新加载时,仅通过该列设置关系,这意味着关系设置“正确”,即使我们在第一次创建数据时没有正确创建关系。
如果我们要管理Email实体中的关系(即,在Message.java中设置mappedBy属性,而不是Email.java,情况将会相反:设置Message's email属性不会反映在数据库中,但是设置Email's message属性会。
以下是对这些观点的总结:
-
您必须显式管理关联的两端。
-
只有对关联所有者的更改才会在数据库中生效。
-
当您从数据库加载一个分离的实体时,它将反映数据库中持久化的外键关系。
表 4-1 显示了如何选择应该成为双向关联所有者的关系方。请记住,要使关联成为所有者,您必须将另一端标记为由另一端映射。
表 4-1
标记关联的所有者
| 一对一 | 任何一端都可以成为所有者,但其中一个(且只有一个)应该成为所有者;如果不指定这一点,将会导致循环依赖。 | | 一对多 | “一”端必须成为关联的所有者。 | | 多对一 | 从相反的角度来看,这与一对多关系相同,因此同样的规则也适用:多端必须成为关联的所有者。 | | 多对多 | 关联的任何一端都可以成为所有者。 |如果这一切看起来相当混乱,请记住关联所有权只与数据库中的外键管理有关,随着您进一步使用 Hibernate,事情会变得更加清楚。关联和映射将在接下来的几章中详细讨论。
保存实体
创建用 Hibernate 映射映射的类的实例不会自动将对象保存到数据库中。在您显式地将对象与有效的 Hibernate 会话相关联之前,该对象是暂时的,就像任何其他 Java 对象一样。在 Hibernate 中,我们使用save()或persist()中的一个,它是Session接口上的save()方法的同义词,来在数据库中存储一个瞬态对象,如下所示:
public Serializable save(Object object)
public Serializable save(String entityName, Object object)
两个save()方法都将一个瞬态对象引用(不能是null)作为参数。Hibernate 希望为瞬态对象的类找到一个映射(注释或 XML 映射);Hibernate 不能 6 持久化任意未映射的对象。如果已经将多个实体映射到一个 Java 类,那么可以用entityName参数指定要保存哪个实体(Hibernate 不会只知道 Java 类名)。
这些save()方法都创建一个新的org.hibernate.event.spi.SaveOrUpdateEvent事件。事件是 Hibernate 中相当高级的主题,大多数读者不需要,但是感兴趣的读者可以在 Hibernate 6 文档的“事件”一章的 https://red.ht/3iGN7tZ 阅读更多内容。
最简单的方法是,我们用 Java 创建一个新对象,设置它的一些属性,然后通过会话保存它。这里有一个简单的对象,如清单 4-11 所示。
package chapter04.model;
import javax.persistence.*;
@Entity
public class SimpleObject {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column
String key;
@Column
Long value;
public SimpleObject() {
}
// mutators and accessors not included for brevity
// equals() and hashCode() will be covered later in this chapter
}
Listing 4-11src/main/java/chapter04/model/SimpleObject.java
清单 4-12 展示了如何在testSaveLoad()方法中保存这个对象,如chapter04.general.SaveLoadTest所示。
package chapter04.general;
import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
public class SaveLoadTest {
@Test
public void testSaveLoad() {
Long id = null;
SimpleObject obj;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
obj = new SimpleObject();
obj.setKey("sl");
obj.setValue(10L);
session.save(obj);
assertNotNull(obj.getId());
// we should have an id now, set by Session.save()
id = obj.getId();
tx.commit();
}
try (Session session = SessionUtil.getSession()) {
// we're loading the object by id
SimpleObject o2 = session.load(SimpleObject.class, id);
assertEquals(o2.getKey(), "sl");
assertNotNull(o2.getValue());
assertEquals(o2.getValue().longValue(), 10L);
SimpleObject o3 = session.load(SimpleObject.class, id);
// since o3 and o2 were loaded in the same session, they're not only
// equivalent - as shown by equals() - but equal, as shown by ==.
// since obj was NOT loaded in this session, it's equivalent but
// not ==.
assertEquals(o2, o3);
assertEquals(obj, o2);
assertEquals(obj, o3);
assertSame(o2, o3);
assertFalse(o2 == obj);
assertSame(obj, o3);
assertFalse(obj == o3);
}
}
}
Listing 4-12src/test/java/chapter04/general/SaveLoadTest.java
保存已经持久化的对象是不合适的。这样做将更新对象,这实际上将最终创建一个具有新标识符的副本。这可以在DuplicateSaveTest,清单 4-13 中看到。
package chapter04.general;
import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.*;
public class DuplicateSaveTest {
@Test
public void duplicateSaveTest() {
Long id;
SimpleObject obj;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
obj = new SimpleObject();
obj.setKey("Open Source and Standards");
obj.setValue(10L);
session.save(obj);
assertNotNull(obj.getId());
id = obj.getId();
tx.commit();
}
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
obj.setValue(12L);
// this is not good behavior!
session.save(obj);
tx.commit();
}
// note that save() creates a new row in the database!
// this is wrong behavior. Don't do this!
assertNotEquals(id, obj.getId());
try (Session session = SessionUtil.getSession()) {
List<SimpleObject> objects=session
.createQuery("from SimpleObject", SimpleObject.class)
.list();
// again, this is a value we DO NOT WANT.
assertEquals(objects.size(), 2);
}
}
}
Listing 4-13src/test/java/chapter04/general/DuplicateSaveTest.java
当这个测试运行时,两个标识符应该是相等的,但是它们不是;检查这些值产生了等效的对象,除了 id,它们是按指定的SimpleObject @Id代顺序分配的。
但是,您可以用Session.saveOrUpdate()(当然也可以用Session.update())更新一个对象。
如果对象不存在,saveOrUpdate()会调用save(),而update()不会;如果您的目标是确保数据库中存在一个对象,那么saveOrUpdate()会稍微安全一些;update()如果数据库中不存在该对象,将会失败并出现异常。例如,如果您试图更新订单发票,这将是合适的;如果它还不存在,您不会想要创建一个。
清单 4-14 显示了另一个类SaveOrUpdateTest。
package chapter04.general;
import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.*;
public class SaveOrUpdateTest {
@Test
public void testSaveOrUpdateEntity() {
Long id;
SimpleObject obj;
try (Session session=SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
// this only works for simple objects
session
.createQuery("delete from SimpleObject")
.executeUpdate();
tx.commit();
}
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
obj = new SimpleObject();
obj.setKey("Open Source and Standards");
obj.setValue(14L);
session.save(obj);
assertNotNull(obj.getId());
id = obj.getId();
tx.commit();
}
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
obj.setValue(12L);
// if the key didn't exist in the database,
// it would after this call.
session.saveOrUpdate(obj);
tx.commit();
}
// saveOrUpdate() will update a row in the database
// if one matches. This is what one usually expects.
assertEquals(id, obj.getId());
try (Session session = SessionUtil.getSession()) {
List<SimpleObject> objects=session
.createQuery("from SimpleObject", SimpleObject.class)
.list();
assertEquals(objects.size(), 1);
}
}
}
Listing 4-14src/test/java/chapter04/general/SaveOrUpdateTest.java
在生产代码中尝试匹配这种代码结构是不明智的。
对象从瞬时状态(创建时)变为持久状态(第一次保存时),然后返回瞬时状态(会话关闭时)。然后,我们在对象处于瞬态时更新它,并在调用Session.saveOrUpdate()时将它移回持久状态。
理想情况下,您首先要做的是从会话中加载对象(就像我们在其他大多数显示更新的例子中所做的那样);这意味着更新发生在持久对象上,我们实际上根本不需要调用Session.save()、Session.update()或Session.saveOrUpdate()。 7 显式调用更新方法之一不是错误,但也不是必须的。
一旦对象处于持久状态,Hibernate 就会在您更改对象的字段和属性时管理数据库本身的更新。
Hibernate 在跟踪更改方面非常有效,它只跟踪更改过的字段。在正常情况下,如果您有一个有 30 个属性的实体,并且更改了一个,Hibernate 将发出一个相当小的 SQL UPDATE来修改数据库记录。
对象相等和相同
当我们讨论 Hibernate 中的持久对象时,我们还需要考虑 Hibernate 中对象相等性和身份扮演的角色。当我们在 Hibernate 中有一个持久对象时,这个对象既代表特定 Java 虚拟机(JVM)中的一个类的实例,也代表数据库表中的一行(或多行)。
从同一个 Hibernate 会话再次请求一个持久对象会返回一个类的同一个 Java 实例,这意味着您可以使用标准的 Java ==等式语法来比较对象。但是,如果您从多个 Hibernate 会话中请求一个持久对象,Hibernate 将从每个会话中提供不同的实例,如果您比较这些对象实例,==操作符将返回false。
考虑到这一点,如果您在两个不同的会话中比较对象,您将需要在您的 Java 持久化对象上实现equals()方法,无论如何您应该经常这样做。(只是别忘了一起实现hashCode()。)
实现equals()可能会很有趣。Hibernate 将实际的对象包装在代理中(出于各种性能增强的原因,比如按需加载数据),所以您需要考虑类层次结构的等价性;不要检查实际类型的等效性,而是检查类型是否为可分配的或兼容的。与实际的字段相比,在你的equals()和hashCode()方法中使用访问器通常更有效。
大多数 ide 会生成equals()和hashCode()来使用实例引用本身,而不是访问器。这对于大多数对象来说非常有效,并且是正确的行为;毕竟,访问器通常是返回引用的单行方法。然而,一个访问器没有有成为一行方法;它可能会创建一个引用的副本或计算一个值,这两种操作可能会也可能不会很昂贵。但是,在 Hibernate 的情况下,调用访问器也使代理有机会从数据库加载属性(如果它还不存在的话),这在大多数情况下是一个重要且有用的特性。
清单 4-15 是我们一直在使用的 SimpleObject 实体的equals()和hashCode()的实现,由 IntelliJ IDEA8生成并修改为使用访问器。
package chapter04.model;
import javax.persistence.*;
@Entity
public class SimpleObject {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column
String key;
@Column
Long value;
public SimpleObject() {
}
// mutators and accessors not included for brevity
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SimpleObject)) return false;
SimpleObject that = (SimpleObject) o;
// we prefer the method versions of accessors, because of Hibernate's proxies.
if (getId() != null
? !getId().equals(that.getId())
: that.getId() != null)
return false;
if (getKey() != null
? !getKey().equals(that.getKey())
: that.getKey() != null)
return false;
return getValue() != null
? getValue().equals(that.getValue())
: that.getValue() == null;
}
@Override
public int hashCode() {
int result = getId() != null ? getId().hashCode() : 0;
result = 31 * result + (getKey() != null ? getKey().hashCode() : 0);
result = 31 * result + (getValue() != null ? getValue().hashCode() : 0);
return result;
}
}
Listing 4-15src/main/java/chapter04/model/SimpleObject.java
TestSaveLoad.java类展示了等式的各种可能性和条件,如我们前面看到的清单 4-12 所示。
注意,在该代码中,o2和o3是等于(它们持有相同的引用),而o2和obj是等价的(引用不同但持有相同的数据)。同样,您不应该在生产代码中依赖这一点;对象等价应该总是用equals()来测试。
加载实体
Hibernate 的Session接口提供了几种从数据库加载实体的load()方法。每个load()方法都需要对象的主键作为标识符。 9
除了 ID 之外,Hibernate 还需要知道使用哪个类名或实体名来查找具有该 ID 的对象。如果您没有将类类型传递给load(),您还需要将结果转换为正确的类型。基本的load()方法如下:
public <T> T load(Class<T> theClass, Object id)
public Object load(String entityName, Object id)
public void load(Object object, Object id)
最后一个load()方法将一个Object作为第一个参数。该对象应该与您想要加载的对象具有相同的类类型,并且应该是空的(即,已构造,但其值对于您的应用来说缺乏意义-例如,考虑使用通过默认构造函数构造的对象)。Hibernate 将使用您请求的对象填充该对象。虽然这类似于 Java 中的其他库调用——即java.util.List.toArray()——但这种语法可能没有太大的实际好处。
其他的load()方法以锁模式作为参数。 10 锁模式指定 Hibernate 是否应该在缓存中查找对象,以及 Hibernate 应该对表示该对象的数据行使用哪个数据库锁级别。Hibernate 开发人员声称 Hibernate 通常会为您选择正确的锁模式,尽管我们已经看到手动选择正确的锁非常重要的情况。此外,您的数据库可以选择自己的锁定策略——例如,锁定整个表而不是表中的多行。按照限制最少到限制最多的顺序,您可以使用以下各种锁定模式: 11
-
NONE:不使用行级锁定,使用缓存对象(如果可用);这是休眠默认设置。 -
READ:防止其他SELECT查询在事务提交之前读取事务中的数据(因此可能是无效的)。 -
UPGRADE:使用SELECT FOR UPDATESQL 语法(或等效语法)锁定数据,直到事务完成。(这个其实已经弃用了;用PESSIMISTIC_WRITE代替。) -
UPGRADE_NOWAIT:使用NOWAIT关键字(对于 Oracle),如果有另一个线程使用该行,则立即返回错误;否则,这就类似于UPGRADE。 -
UPGRADE_SKIPLOCKED:跳过已经被其他更新锁定的行的锁定,但在其他方面类似于UPGRADE。 -
OPTIMISTIC:该模式假设更新不会经历争用。该实体的内容将在接近事务结束时得到验证。 -
OPTIMISTIC_FORCE_INCREMENT:这类似于OPTIMISTIC,除了它强制对象的版本在接近事务结束时递增。 -
PESSIMISTIC_READ和PESSIMISTIC_WRITE:这两个都在访问行时立即获得锁。 -
PESSIMISTIC_FORCE_INCREMENT:这将在访问行时立即获得锁,并立即更新实体版本。
所有这些锁定模式都是org.hibernate.LockMode枚举上的静态字段。(我们将在第八章更详细地讨论与事务相关的锁定和死锁。)使用锁定模式的load()方法如下:
public <T> T load(Class<T> theClass, Object id, LockMode lockMode)
public Object load(String entityName, Object id, LockMode lockMode)
除非你确定对象存在,否则不应该使用load()方法。如果您不确定,那么使用get()方法之一。如果在数据库中没有找到惟一的 ID,load()方法将抛出一个异常,而get()方法将仅仅返回一个空引用。
与load()非常相似,get()方法接受一个标识符和一个实体名或一个类。还有两个get()方法将锁定模式作为参数。get()方法如下:
public <T> T get(Class<T> entityType, Object id)
public Object get(String entityName, Object id)
public <T> T get(Class<T> entityType, Object id, LockMode lockMode)
public Object get(String entityName, Object id, LockMode lockMode)
也有使用LockOption的load和get变体,但是大多数用户最终会指定映射到LockMode特性的组合。
如果您需要确定给定对象的实体名称(默认情况下,这与类名相同),您可以在Session接口上调用getEntityName()方法,如下所示:
public String getEntityName(Object object)
使用get()和load()方法很简单。例如,通过 web 应用,某人可以为 ID 为 1 的供应商选择一个Supplier详细页面 12 。如果我们不确定供应商是否存在,我们使用get()方法来检查是否为空,如下所示:
// get an id from some other Java class, for instance, through a web application
Supplier supplier = session.get(Supplier.class,id);
if (supplier == null) {
System.out.println("Supplier not found for id " + id);
return;
}
我们还可以从 Hibernate 中检索实体名称,并将其用于get()或load()方法。如上所述,如果找不到具有该 ID 的对象,load()方法将抛出一个异常。
String entityName = session.getEntityName(supplier);
Supplier secondarySupplier = (Supplier) session.load(entityName,id);
还值得指出的是,您可以查询实体,这允许您查找具有特定标识符的对象,以及匹配其他标准的对象集。还有一个 Criteria API,允许您使用声明性机制来构建查询。这些主题将在后面的章节中讨论。
合并实体
当您希望将分离的实体再次更改为持久状态时,将执行合并,并将分离的实体的更改迁移到(或覆盖)数据库。合并操作的方法签名如下:
Object merge(Object object)
Object merge(String entityName, Object object)
合并与refresh()相反,它用数据库中的值覆盖分离实体的值。首先,让我们构建一个实用方法(在它自己的类中)来帮助我们验证一个对象的值。
package chapter04.general;
import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import static org.testng.Assert.assertEquals;
public class ValidateSimpleObject {
public static SimpleObject validate(
Long id,
Long expectedValue,
String expectedKey) {
SimpleObject so = null;
try (Session session = SessionUtil.getSession()) {
// will throw an Exception if the id isn't found
// in the database
so = session.load(SimpleObject.class, id);
assertEquals(so.getKey(), expectedKey);
assertEquals(so.getValue(), expectedValue);
}
return so;
}
}
Listing 4-16src/test/java/chapter04/general/ValidateSimpleObject.java
现在我们可以看一看MergeTest。
package chapter04.general;
import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
public class MergeTest {
@Test
public void testMerge() {
Long id;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
SimpleObject simpleObject = new SimpleObject();
simpleObject.setKey("testMerge");
simpleObject.setValue(1L);
session.save(simpleObject);
id = simpleObject.getId();
tx.commit();
}
SimpleObject so = ValidateSimpleObject.validate(id, 1L, "testMerge");
// the 'so' object is detached here.
so.setValue(2L);
try (Session session = SessionUtil.getSession()) {
// merge is potentially an update, so we need a TX
Transaction tx = session.beginTransaction();
session.merge(so);
tx.commit();
}
ValidateSimpleObject.validate(id, 2L, "testMerge");
}
}
Listing 4-17src/test/java/chapter04/general/MergeTest.java
这段代码创建一个实体(a SimpleObject)然后保存它;然后它验证对象的值(用来自ValidateSimpleObject的validate()方法),这本身返回一个分离的实体。我们更新分离的对象并merge()它——它应该更新数据库中写入的值,这是我们验证的。
刷新实体
Hibernate 提供了一种机制来刷新持久对象的数据库表示,覆盖内存中对象可能有的值。使用会话接口上的refresh()方法之一来刷新持久对象的实例,如下所示:
public void refresh(Object object)
public void refresh(Object object, LockMode lockMode)
如上所述,这些方法将从数据库重新加载对象的属性,覆盖它们;因此,refresh()是merge()的逆。合并用先前瞬态对象的值覆盖数据库,而refresh()用数据库中的值覆盖瞬态对象中的值。
Hibernate 通常会很好地为您处理这个问题,所以您不必经常使用refresh()方法。然而,也有 Java 对象表示与对象的数据库表示不同步的情况。例如,如果您使用 SQL 来更新数据库,Hibernate 将不会意识到表示发生了变化。但是,您不需要经常使用这种方法。 13 与load()方法类似,refresh()方法可以以一个锁模式作为自变量;请参阅上一节“加载实体”中对锁模式的讨论。
让我们看看清单 4-18 中使用 refresh()的代码——基本上是我们看到的演示merge()的代码的逆。
package chapter04.general;
import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
public class RefreshTest {
@Test
public void testRefresh() {
Long id;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
SimpleObject simpleObject = new SimpleObject();
simpleObject.setKey("testMerge");
simpleObject.setValue(1L);
session.save(simpleObject);
id = simpleObject.getId();
tx.commit();
}
SimpleObject so = ValidateSimpleObject.validate(id, 1L, "testMerge");
// the 'so' object is detached here
so.setValue(2L);
try (Session session = SessionUtil.getSession()) {
// note that refresh is a read,
// so no TX is necessary unless an update occurs later
session.refresh(so);
}
ValidateSimpleObject.validate(id, 1L, "testMerge");
}
}
Listing 4-18src/test/java/chapter04/general/RefreshTest.java
这段代码与merge()测试相同,有两处变化:第一处是它调用了refresh()而不是merge()(惊喜!);另一个是它期望对象的数据从数据库恢复到原始状态,验证refresh()覆盖了瞬态对象的数据。
在本书之前的版本中,merge()和refresh()测试——以及他们使用的validate()方法——都在同一个类中。在这里,它们被分开,主要是因为这允许使用完整的源代码清单。
更新实体
Hibernate 自动将对持久对象所做的更改保存到数据库中。 14 如果一个持久对象的属性发生了变化,相关的 Hibernate 会话将使用 SQL 把这个变化排队保存到数据库中。从开发人员的角度来看,您不需要做任何工作来存储这些更改,除非您想强制 Hibernate 提交队列中的所有更改。您还可以确定会话是否是脏的,是否需要提交更改。当您提交 Hibernate 事务时,Hibernate 会为您处理这些细节。
flush()方法强制 Hibernate 刷新会话,如下所示:
public void flush() throws HibernateException
您可以使用 is dirty()方法确定会话是否脏,如下所示:
public boolean isDirty() throws HibernateException
您还可以通过setHibernateFlushMode() 15 方法指示 Hibernate 为会话使用刷新模式。getHibernateFlushMode()方法返回当前会话的刷新模式,如下所示:
public void setHibernateFlushMode(FlushMode flushMode)
public FlushMode getHibernateFlushMode()
可能的冲洗模式如下:
-
ALWAYS:每个查询在执行之前都会刷新会话。这会非常慢。 -
AUTO: Hibernate 管理查询刷新,保证查询返回的数据是最新的。 -
COMMIT: Hibernate 在事务提交时刷新会话。 -
MANUAL:您的应用需要使用 flush()方法来管理会话刷新。Hibernate 从不刷新会话本身。
默认情况下,Hibernate 使用AUTO刷新模式。通常,您应该使用事务边界来确保进行适当的刷新,而不是试图在适当的时间“手动”刷新。
删除实体
为了方便从数据库中删除实体,Session接口提供了一个delete()方法,如下所示:
public void delete(Object object)
public void delete (String entityName, Object object)
这个方法接受一个持久对象作为参数。该参数也可以是一个瞬态对象,其标识符设置为需要擦除的对象的 ID。
在最简单的形式中,您只是删除一个与其他对象没有关联的对象,这很简单;但是许多对象确实与其他对象有关联。为了实现这一点,Hibernate 可以被配置为允许从一个对象到其相关对象的级联删除。
例如,考虑这样一种情况,您有一个包含子对象集合的父对象,您想删除所有子对象。处理这个问题最简单的方法是在 Hibernate 映射中对集合的元素使用 cascade 属性。如果将“级联”属性设置为“删除”或“全部”,删除将级联到所有关联的对象。Hibernate 会帮你删除这些:删除父对象会删除相关的对象。
Hibernate 还支持批量删除,即应用对数据库执行删除 HQL 语句。这对于一次删除多个对象非常有用,因为每个对象不需要为了删除而加载到内存中,如下所示:
session.createQuery("delete from User").executeUpdate();
与针对每个实体标识符单独发出delete()调用相比,网络流量大大减少,内存需求也大大减少。
批量删除不会导致级联操作的执行。如果需要级联行为,您将需要自己执行适当的删除(就像使用 SQL 一样)或使用会话的delete()方法。
级联操作
当您在实体上执行本章中描述的操作之一时,这些操作不会在关联的实体上执行,除非您明确地告诉 Hibernate 执行它们。当操作影响关联的实体时,它们被称为“级联”操作,因为操作从一个对象流向另一个对象。
例如,当我们尝试提交事务时,清单 4-19 中的代码将会失败,因为与Email实体相关联的Message实体还没有被持久化到数据库中,所以Email实体不能在它的表中被准确地表示(用它的外键表示到适当的消息行上)。
try(Session session = SessionUtil.getSession()) {
Transaction tx=session.beginTransaction();
Email email = new Email("Email title");
Message message = new Message("Message content");
email.setMessage(message);
message.setEmail(email);
session.save(email);
tx.commit();
}
Listing 4-19A Failed save() Due to Cascading
理想情况下,我们希望保存操作从电子邮件实体传播到其关联的消息对象。我们通过为实体的属性和字段设置级联操作(或者为整个实体分配一个适当的默认值)来实现这一点。因此,如果至少为电子邮件实体的消息属性设置了PERSIST级联操作,清单 4-19 中的代码将正确执行。Java 持久化 API 支持的级联类型如下:
-
PERSIST -
MERGE -
REFRESH -
REMOVE -
DETACH -
ALL
值得指出的是,Hibernate 有自己的级联配置选项, 16 代表了其中的一个超集;然而,我们很大程度上遵循 Java 持久化 API 规范进行建模,因为这通常比特定于 Hibernate 的建模更常见: 17
-
CascadeType.PERSIST表示save()或persist()业务级联到相关实体;对于我们的Email和Message示例,如果Email's @OneToOne注释包含PERSIST,保存Email也会保存Message。 -
CascadeType.MERGE表示当所属实体合并时,相关实体合并为托管状态。 -
CascadeType.REFRESH对refresh()操作做同样的事情。 -
CascadeType.REMOVE删除所有实体时,删除与此设置相关的所有相关实体。 -
如果要进行手动分离,则分离所有相关实体。
-
CascadeType.ALL是所有级联操作的简写。
级联配置选项接受一组CascadeType引用;因此,要在一对一关系的级联操作中仅包括刷新和合并,您可能会看到以下内容:
@OneToOne(cascade={CascadeType.REFRESH, CascadeType.MERGE})
EntityType otherSide;
还有一个级联操作不是正常集合的一部分,称为孤儿移除,当一个拥有的对象从其拥有关系中移除时,它会从数据库中移除该拥有的对象。但是,不建议将其用作级联类型;建议使用注释选项orphanRemoval,这样@OneToMany的注释可能看起来像OneToMany(orphanRemoval=true)。
假设我们有一个Library实体,它包含一个Book实体的列表。这是我们在Library和Book的列表。
package chapter04.orphan;
import javax.persistence.*;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column
String title;
@ManyToOne
Library library;
public Book() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Library getLibrary() {
return library;
}
public void setLibrary(Library library) {
this.library = library;
}
}
Listing 4-21src/main/java/chapter04/orphan/Book.java
package chapter04.orphan;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Library {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column
String name;
@OneToMany(orphanRemoval = true, mappedBy = "library")
List<Book> books = new ArrayList<>();
public Library() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Book> getBooks() {
return books;
}
public void setBooks(List<Book> books) {
this.books = books;
}
}
Listing 4-20src/main/java/chapter04/orphan/Library.java
请注意在@OneToMany 注释中使用了 orphanRemoval。现在让我们看一些测试代码,这些代码相当冗长,因为我们需要验证我们的初始数据集,更改它,然后重新验证;参见清单 4-22 。
package chapter04.orphan;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
public class OrphanRemovalTest {
@Test
public void orphanRemovalTest() {
Long id = createLibrary();
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Library library = session.load(Library.class, id);
assertEquals(library.getBooks().size(), 3);
library.getBooks().remove(0);
assertEquals(library.getBooks().size(), 2);
tx.commit();
}
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Library l2 = session.load(Library.class, id);
assertEquals(l2.getBooks().size(), 2);
Query<Book> query = session
.createQuery("from Book b", Book.class);
List<Book> books = query.list();
assertEquals(books.size(), 2);
tx.commit();
}
}
@Test
public void deleteLibrary() {
Long id = createLibrary();
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Library library = session.load(Library.class, id);
assertEquals(library.getBooks().size(), 3);
session.delete(library);
tx.commit();
}
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Library library = session.get(Library.class, id);
assertNull(library);
List<Book> books=session
.createQuery("from Book b", Book.class)
.list();
assertEquals(books.size(), 0);
}
}
private Long createLibrary() {
Library library = null;
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
library = new Library();
library.setName("orphanLib");
session.save(library);
Book book = new Book();
book.setLibrary(library);
book.setTitle("book 1");
session.save(book);
library.getBooks().add(book);
book = new Book();
book.setLibrary(library);
book.setTitle("book 2");
session.save(book);
library.getBooks().add(book);
book = new Book();
book.setLibrary(library);
book.setTitle("book 3");
session.save(book);
library.getBooks().add(book);
tx.commit();
}
return library.getId();
}
}
Listing 4-22src/test/java/chapter04/orphan/OrphanRemovalTest.java
这并不复杂:它构建了一个包含三本书的图书馆。然后,它从数据库加载图书馆,验证它看起来像它应该的那样(“一个有三本书的图书馆”),并从图书馆中删除一本。它不删除被删除的Book实体;它只是将它从图书馆的图书集中删除,从而使它成为孤儿。
在提交了图书馆对象的新状态之后——通过tx.commit()——我们从数据库中重新加载图书馆,并验证它现在只有两本书。我们搬走的那本书从图书馆不见了。
但是,这并不意味着它实际上已经被删除了,所以我们然后查询数据库中所有的Book实体,看看我们是否有两个或三个。我们应该只有两个,事实也的确如此。更新库时,我们删除了孤立对象。
如果你想让这本书在搬走后还在,或者被分配到其他图书馆,那么orphanRemoval是不正确的;你会希望这本书能够作为一个孤儿存在。
延迟加载、代理和集合包装器
考虑典型的互联网 web 应用:在线商店。这家商店有一份产品目录。在最粗糙的层次上,这可以被建模为管理一系列产品实体的目录实体。在大型商店中,可能有成千上万的产品被分成不同的重叠类别。
当客户访问商店时,必须从数据库中加载目录。我们可能不希望实现将代表成千上万个产品的每个实体都加载到内存中。对于一个足够大的零售商来说,考虑到机器上可用的物理内存量,这甚至是不可能的。即使这是可能的,它也可能会削弱网站的性能。
相反,我们只希望加载目录,也可能加载类别。只有当用户深入到类别中时,才应该从数据库中加载该类别中产品的子集。
为了解决这个问题,Hibernate 提供了一个名为 lazy loading 的工具。启用时(这是使用 XML 映射时的默认设置,而不是使用注释时的默认设置,默认设置为急切加载,实体的关联实体只有在被直接请求时才会被加载,这可以提供相当可观的性能优势,这是可以想象的。例如,以下代码仅从数据库中加载一个实体:
Email email = session.get(Email.class,new Integer(42));
但是,如果访问了类的关联,并且延迟加载生效,则仅在需要时从数据库中提取关联。例如,在下面的代码片段中,关联的消息对象将被加载,因为它被显式引用:
// surely this email is about the meaning of life, the universe, and everything
Email email = session.get(Email.class,new Integer(42));
String text = email.getMessage().getContent();
Hibernate 将这种行为强加于您的实体的最简单方法是提供它们的代理实现。 18 Hibernate 通过替换从实体的类中派生的代理来拦截对实体的调用。如果缺少请求的信息,那么在将控制权交给父实体的实现之前,将从数据库中加载该信息。在将关联表示为集合类的情况下,会创建一个包装器(本质上是集合的代理,而不是它所包含的实体的代理)并替换原始集合。
Hibernate 只能通过会话访问数据库。如果当我们试图访问一个还没有加载的关联(通过代理或集合包装器)时,一个实体从会话中分离出来,Hibernate 抛出一个LazyInitializationException。解决方法是通过将实体附加到会话来确保实体再次持久化,或者在实体从会话分离之前访问所有需要的字段。
如果您需要确定一个代理、一个持久化集合或者一个属性是否已经被延迟加载,您可以调用org.hibernate.Hibernate类上的isInitialized(Object proxy)和isPropertyInitialized(Object proxy, String propertyName)方法。您还可以通过调用org.hibernate.Hibernate类上的initialize(Object proxy)方法来强制代理或集合完全填充。如果使用此方法初始化集合,还需要初始化集合中包含的每个对象,因为只有集合保证会被初始化。
查询对象
Hibernate 提供了几种不同的方法来查询存储在数据库中的对象。显然,如果您已经知道一个对象的标识符,您可以使用该标识符从数据库中加载它。标准查询 API 是一个 Java API,用于将查询构造为一个对象。HQL 是一种面向对象的查询语言,类似于 SQL,您可以使用它来检索与查询匹配的对象。我们将在第九章和第十章中进一步讨论这些问题。如果您有使用 SQL 的遗留应用,或者如果您需要使用 HQL 和条件查询 API 不支持的 SQL 特性,Hibernate 提供了一种直接对数据库执行 SQL(通过“原生查询”)以检索对象的方法。
摘要
Hibernate 提供了一个简单的 API,用于通过会话接口创建、检索、更新和删除关系数据库中的对象。理解 Hibernate 中瞬时对象、持久对象和分离对象之间的区别,将使您理解对对象的更改如何更新数据库表。
我们已经提到了创建映射的需要,以便将数据库表与您想要持久化的 Java 对象的字段和属性相关联。下一章将详细介绍这些,并讨论为什么需要它们以及它们可以包含什么。
*五、映射概述
Hibernate 的目的是允许您将数据库视为存储 Java 对象。然而,在实践中,关系数据库不存储对象——它们将数据存储在表和列中。不幸的是,没有简单的方法可以将存储在关系数据库中的数据与 Java 对象表示的数据一致地关联起来。 1
面向对象的关联和关系的关联之间的区别是根本性的。考虑一个简单的类来表示用户,另一个类来表示电子邮件地址,如图 5-1 所示。

图 5-1
简单的实体关系图
这里,User对象包含引用Email对象的字段。协会有方向;给定一个User对象,您可以确定其关联的Email对象。比如考虑上市 5-1 。
User user = getUserSomehow();
Email email = user.email;
Listing 5-1Acquiring the Email Object
from User
然而,反之则不然。在数据库中表示这种关系的自然方式,如图 5-2 所示,表面上是相似的。

图 5-2
相关的联系
尽管如此,这种联系的方向实际上是相反的。给定一个Email行,您可以立即确定它在数据库中属于哪个用户行;这种关系是由外键约束规定的。通过适当使用 SQL,有可能逆转数据库世界中的关系——这是另一个不同之处。
考虑到这两个世界之间的差异,有必要手动干预以确定您的 Java 类应该如何在数据库表中表示。
为什么映射不容易实现自动化
为什么不能创建简单的规则来将 Java 对象存储在数据库中,以便可以轻松地检索它们,这并不总是显而易见的。例如,最显而易见的规则是 Java 类必须与单个表相关。例如,图 5-2 中定义的User类的实例肯定可以用一个简单的表来表示,如清单 5-2 所示。
public class User {
String name;
String password;
}
Listing 5-2A Simple User Class
with a Password
确实可以,但一些问题出现了:
-
如果你保存一个用户两次,你会得到多少行?
-
你可以保存一个没有名字的用户吗?
-
允许你保存一个没有密码的用户吗?
当您开始考虑引用其他类的类时,还有一些额外的问题需要考虑。看看清单 5-3 中显示的客户和电子邮件类。
public class Customer {
int customerId;
int customerReference;
String name;
Email email;
}
public class Email {
String address;
}
Listing 5-3Customer and Email Classes
基于此,出现了以下问题:
-
唯一客户是通过其客户 ID 还是客户证明来识别的?
-
一个电子邮件地址可以被多个客户使用吗?
-
客户可以有多个电子邮件 ID 吗?
-
客户表中是否应该表示这种关系?
-
是否应该在电子邮件表中表示这种关系?
-
这种关系应该在第三个(链接)表中表示吗?
根据对这些问题的回答,您的数据库表可能会有很大的不同。你可以尝试一个合理的设计,如图 5-3 所示,基于你对现实世界中可能发生的情况的直觉。

图 5-3
相关的联系
这里,我们有一个表,其中一个客户有一个假的客户 Id;电子邮件地址只能由一个客户使用,关系由Email表维护。
让 JBoss Tools 或 IDEA Ultimate 之类的工具从数据库表中生成 Hibernate 实体是完全可能的(实际上也相当常见),因为工具可以访问实际的数据库结构;工具可以看出customerId是Email的外键,期望在Customer表中有相应的值,并且可以找出合适的名称(列名),这可能是一种惯用的映射。
但是 Hibernate 并不强制要求这些;你可以拥有 Hibernate 可以映射的多层模式关系,但是一个工具可能会设计得很糟糕。
主键
大多数提供 SQL 访问的关系数据库都准备接受没有预定义主键的表。Hibernate 没那么宽容;即使创建的表没有主键,Hibernate 也会要求您指定一个主键。对于熟悉 SQL 和数据库,但不熟悉 ORM 工具的用户来说,这似乎有悖常理。因此,让我们更深入地检查没有主键时出现的问题。
首先,如果没有主键,就不可能(容易地)唯一标识表中的一行。例如,考虑表 5-1 。
表 5-1
一种表,其中的行不容易被唯一标识
|用户
|
年龄
|
| --- | --- |
| dminter | 35 |
| dminter | 40 |
| dminter | 55 |
| dminter | 40 |
| jlinwood | 57 |
该表清楚地包含了关于用户及其各自年龄的信息。然而,有四个用户具有相同的标识符(戴夫明特、丹尼斯明特、丹尼尔明特和达希尔明特)。在系统中的其他地方可能有办法区分他们——也许是通过电子邮件地址或用户号码。但是如果你想知道用户 ID 为 32 的 Dashiel Minter 的年龄,就没有办法从表 5-1 中获取。
虽然 Hibernate 不允许您省略主键,但是它允许您从一组列中形成主键,形成一个“组合键”例如,表 5-2 可以用组合键usernumber和user来标识。
表 5-2
可能存在复合主键的表
|用户
|
用户编号
|
年龄
|
| --- | --- | --- |
| dminter | 1 | 35 |
| dminter | 2 | 40 |
| dminter | 3 | 55 |
| dminter | 32 | 40 |
| jlinwood | 1 | 57 |
User和Usernumber都不包含唯一的条目,但是它们组合起来唯一地标识了特定用户的年龄,因此它们可以作为主键被 Hibernate 接受。
为什么 Hibernate 需要唯一标识条目,而 SQL 不需要?因为 Hibernate 表示的是 Java 对象,这些对象总是唯一可识别的。新 Java 开发人员犯的经典错误是使用==操作符而不是equals()方法来比较字符串。您可以区分对代表相同文本的两个 String 对象的引用和对同一String对象的两个引用。 2 SQL 没有这样的义务,可以说在某些情况下,放弃进行区分的能力是可取的。
例如,如果 Hibernate 不能用主键唯一地标识一个对象,那么下面的代码在基础表中可能会有几种结果:
String customer = getCustomerFromHibernate("dcminter");
customer.setAge(10);
saveCustomerToHibernate(customer);
假设该表最初包含表 5-3 中所示的数据。
表 5-3
更新不明确的表
|用户
|
年龄
|
| --- | --- |
| dminter | 35 |
| dminter | 40 |
结果表中应包含以下哪一项?
-
用户 dcminter 的单行,年龄设置为 10
-
用户的两行,两个年龄都设置为 10 岁
-
用户的两行,一行年龄设置为 10,另一行设置为 40(更新第一个
dminter记录) -
用户的两行,一行年龄设置为 10,另一行设置为 30(更新第二个记录)
-
用户有三行,一行年龄设置为 10,其他的年龄设置为 35 和 40(总共创建一个新的
dminter记录)
有很多关于我们虚构的saveCustomerToHibernate()在这里做什么的假设,其中一些表面上听起来绝对疯狂…但是这里考虑的想法是,如果我们说“将记录的年龄设置为一个给定值”,数据库的状态可能是什么?**
*简而言之,Hibernate 开发人员决定在创建映射时强制使用主键,这样就不会出现这个问题。Hibernate 确实提供了一些工具,允许您在绝对必要的情况下解决这个问题(您可以创建视图或存储过程来“伪造”适当的键,或者您可以使用传统的 JDBC 来访问表数据),但是在使用 Hibernate 时,如果可能的话,最好是使用正确指定了主键的表。
惰性装载
当您将类从数据库加载到内存中时,您不一定希望所有的信息都被加载。举一个(相当)极端的例子,加载电子邮件列表不应该导致每封电子邮件的全文和附件都被加载到内存中。
首先,电子邮件的全部内容可能需要比实际可用内存更多的内存。
第二,即使电子邮件能够被存储,也可能需要很长时间才能获得所有这些信息。(请记住,数据通常通过网络从数据库进程传输到应用,即使您的网络很快,数据传输仍然需要时间。)
如果您要在 SQL 中解决这个问题,您可能会为查询选择适当字段的子集以获得列表,或者限制数据的范围。以下是选择数据子集的示例:
SELECT from, to, date, subject FROM email WHERE username = 'dcminter';
Hibernate 将允许您设计与此非常相似的查询,但它也提供了一种更灵活的方法,称为延迟加载。某些关系可以被标记为“懒惰”,它们不会从数据库中被加载,直到它们被真正需要。
Hibernate 的默认设置是类(包括像Set和List这样的集合)应该被延迟加载。例如,当从数据库加载下一个清单中给出的 User 类的实例时,加载后立即初始化的字段只有userId和username : 3
public class User {
int userId;
String username;
EmailAddress emailAddress;
Set<Role> roles;
}
根据这一定义,如果会话仍处于活动状态,那么当访问emailAddress和roles的适当对象时,将从数据库中加载这些对象。
这只是默认行为;映射可用于指定哪些类和字段应该以这种方式运行。
联合
当我们看到为什么映射过程不能自动化时,我们讨论了 Java 中可能看起来像这样的类:
public class Customer {
int customerId;
int customerReference;
String name;
StreetAddress address;
}
public class StreetAddress {
String address;
}
我们也给出了它所提出的以下五个问题:
-
唯一客户是通过其客户 ID 还是客户证明来识别的?
-
一个给定的电子邮件地址可以被多个客户使用吗?
-
客户表中是否应该表示这种关系?
-
是否应该在电子邮件表中表示这种关系?
-
这种关系应该在第三个(链接)表中表示吗?
第一个问题可以简单回答;这取决于您将哪个列指定为主键。剩下的四个问题是相关的,它们的答案取决于对象关系。此外,如果您的客户类使用集合类或数组来表示与 EmailAddress 的关系,那么一个用户可能会有多个电子邮件地址: 4
public class Customer {
int customerId;
int customerReference;
String name;
Set<EmailAddress> email;
}
所以,你应该再加一个问题:一个客户可以有一个以上的邮箱吗?该集合可能包含单个条目,因此您不能自动推断出这种情况。
先前选项的关键问题如下:
-
Q1:一个电子邮件地址可以属于多个用户吗?
-
Q2:客户可以有一个以上的电子邮件地址吗?
这些问题的答案可以形成一个真值表,如表 5-4 所示。
表 5-4
决定实体关系的基数
|Q1 回答
|
Q2 回答
|
Customer和Email的关系
|
| --- | --- | --- |
| 不 | 不 | 一对一 |
| 是 | 不 | 多对一 |
| 不 | 是 | 一对多 |
| 是 | 是 | 多对多 |
这是表示对象之间关系的基数 5 的四种方式。然后,可以用各种方式在映射表中表示每个关系。
一对一的联系
类之间的一对一关联可以用多种方式表示。最简单的方法是,在同一个表中维护两个类的属性。例如,用户和电子邮件类之间的一对一关联可以表示为单个表,如表 5-5 所示。
表 5-5
一个组合的User / Email表
身份
|
用户名
|
电子邮件
|
| --- | --- | --- |
| 1 | dminter | dminter@example.com |
| 2 | jlinwood | jlinwood@example.com |
| 3 | jbo | whackadoodle@example.com |
或者,实体可以用相同的主键(如此处所示)在不同的表中维护,或者用一个键从一个实体维护到另一个实体,如表 5-6 和 5-7 所示。
表 5-7
Email一对一的牌桌
身份
|
电子邮件
|
| --- | --- |
| 1 | dminter@example.com |
| 2 | jlinwood@example.com |
| 3 | whackadoodle@example.com |
表 5-6
User一对一的牌桌
身份
|
用户名
|
| --- | --- |
| 1 | dminter |
| 2 | jlinwood |
| 3 | jbo |
可以创建从一个实体到另一个实体的强制外键关系,但是这不应该在两个方向上都应用,因为这样会创建循环依赖。也可以完全省略外键关系,依靠 Hibernate 来管理键的选择和分配。
使用外键关系!如果您的数据集非常非常小,它们可能不会有太大帮助——但在任何真实的数据库环境中,它们可以防止处理数据时出现一些真正令人沮丧的延迟。
如果这两个表不适合共享主键,那么可以维护这两个表之间的外键关系,对外键列应用一个UNIQUE约束。例如,重用我们刚刚看到的User表,可以适当地填充Email表,如表 5-8 所示。
表 5-8
与辅助外键一对一的Email表
身份
|
使用者辩证码
|
电子邮件
|
| --- | --- | --- |
| 34 | 1 | dminter@example.com |
| 37 | 2 | jlinwood@example.com |
| 639 | 3 | whackadoodle@example.com |
这样做的好处是,通过删除外键列上的唯一约束,关联可以很容易地从一对一变为多对一。
一对多和多对一关联
一对多关联(或者从另一个类的角度来看,多对一关联)可以简单地用外键来表示,没有额外的约束。
该关系也可以通过使用链接表来维护。这将在每个关联表中维护一个外键,该外键本身将形成链接表的主键。对于多对多的关系,链接表实际上是强制性的,但是对于在关系的一端基数为 1 的关系,当关系的状态没有在对象本身中反映出来时(比如在List中某个东西可能在哪里),或者当对象不应该有对另一个实体的显式引用时,倾向于使用链接表。
表 5-11
在 1:M 关系中连接电子邮件和用户的链接表
|使用者辩证码
|
电子邮件 ID
|
| --- | --- |
| 1 | 1 |
| 1 | 2 |
| 2 | 3 |
| 2 | 4 |
表 5-10
简单的电子邮件表
|身份
|
电子邮件
|
| --- | --- |
| 1 | dcminter@example.com |
| 2 | dave@example.com |
| 3 | jlinwood@example.com |
| 4 | jeff@example.com |
表 5-9
一个简单的用户表
|身份
|
用户名
|
| --- | --- |
| 1 | dcminter |
| 2 | jlinwood |
可以将附加列添加到链接表中,以维护关联中实体的排序信息。
必须将唯一约束应用于关系的“一”方(表 5-11 中用户电子邮件表的UserID列);否则,链接表可以表示User和Email实体之间所有可能关系的集合,这是一个多对多的集合关联。
多对多关联
正如上一节末尾所提到的,如果在使用链接表时没有对关系的“一”端应用惟一的约束,那么它就变成了一种有限的多对多关系。可以表示User和Email的所有可能组合,但是对于同一个用户来说,不可能将同一个电子邮件地址实体关联两次,因为这需要复制复合主键。
如果不是将外键一起用作复合主键,而是给链接表一个自己的主键(通常是一个代理键),那么两个实体之间的关联就可以转化为完全的多对多关系,如表 5-12 所示。
表 5-12
多对多用户/电子邮件链接表
|身份
|
使用者辩证码
|
电子邮件 ID
|
| --- | --- | --- |
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 1 | 3 |
| 4 | 1 | 4 |
| 5 | 2 | 1 |
| 6 | 2 | 2 |
表 5-12 可能描述了一种情况,其中用户dcminter接收发送到四个地址中任何一个的所有电子邮件,而jlinwood只接收发送到他自己账户的电子邮件。(“EmailId”是接收的电子邮件地址,两个用户 Id 都引用编号为 1 和 2 的电子邮件地址;只有dcminter有 id 为 3 和 4 的电子邮件地址的参考。)
当链接表有自己独立的主键时,应该考虑这样一种可能性,即需要创建一个新的类来将链接表的内容表示为一个独立的实体。这允许您在链接对象中嵌入一个附加状态(比如“这个电子邮件地址被使用了多少次?”).
将映射应用于关联
应用映射来表达在基础表中形成关联的各种不同方式;没有绝对正确的方法来表示它们。 6
除了基本的方法选择之外,映射还用于指定表表示的细节。虽然 Hibernate 倾向于尽可能使用合理的默认值,但通常最好覆盖这些值。例如,Hibernate 自动生成的外键名实际上是随机的,而有见识的开发人员可以应用一个名称(例如,FK_USER_EMAIL_LINK)来帮助在运行时调试违反约束的情况。
其他支持的功能
虽然 Hibernate 可以为映射确定许多合理的默认值,但是大多数默认值都可以被基于注释和基于 XML 的 7 方法中的一种或两种方法覆盖。有些直接应用于映射;其他的,比如外键名,实际上只有在映射用于创建数据库模式时才是相关的。最后,一些映射还可以提供一个配置一些特性的地方,这些特性可能不是最纯粹意义上的“映射”。
除了已经提到的特性之外,本章的最后几节还将讨论 Hibernate 支持的特性。
(数据库)列类型和大小的规范
Java 提供了基本类型,并允许用户声明接口和类来扩展这些类型。关系数据库通常提供一小部分“标准”类型,然后提供额外的专有类型。
将自己局限于专有类型仍然会导致问题,因为在这些类型和 Java 原语类型之间只有近似的对应关系。
有问题类型的一个典型例子是java.lang.String(Hibernate 将其视为原始类型,因为它被频繁使用),默认情况下,它将被映射到固定大小的字符数据数据库类型。通常,如果选择了一个无限大小的字符字段,数据库的性能会很差,但是冗长的String字段会被截断,因为它们被持久化到数据库中。在大多数数据库中,您会选择将一个冗长的String字段表示为TEXT、CLOB或 long VARCHAR类型(假设数据库支持特定类型)。这就是 Hibernate 不能为您完成所有映射的原因之一,也是您在创建使用 ORM 的应用时仍然需要理解一些数据库基础知识的原因之一。
通过指定映射细节,开发人员可以在存储空间、性能和对原始 Java 表示的保真度之间做出适当的权衡。
继承关系到数据库的映射
没有 SQL 标准来表示表中数据的继承关系;虽然一些数据库实现为此提供了专有的语法,但并不是所有的都这样。Hibernate 提供了几种可配置的方法来表示继承关系,这种映射允许用户为他们的模型选择合适的方法。
主关键字
正如本章前面所述(在“主键”一节中),Hibernate 要求使用主键来标识实体。可以通过配置来选择代理键、从业务数据中选择的键和/或复合主键。
当使用代理键时,Hibernate 还允许从一系列可移植性和效率不同的技术中选择键生成技术。(这显示在第四章的“标识符”部分。)
使用基于 SQL 公式的属性
有时,希望实体的属性不是作为直接存储在数据库中的数据来维护,而是作为对该数据执行的函数来维护——例如,小计字段不应该由 Java 逻辑直接管理,而是作为其他一些属性的聚合函数来维护。
强制性和唯一性约束
除了主键或外键关系的隐式约束,您还可以指定字段不能重复——例如,username字段通常应该是唯一的。 8
字段也可以是强制性的,例如,要求消息实体同时具有主题和消息文本。生成的数据库模式将包含相应的NOT NULL和UNIQUE约束,因此用无效数据破坏表非常非常困难(相反,如果试图这样做,应用逻辑将抛出异常)。
请注意,主键隐含地既是强制的又是唯一的。
摘要
本章概述了为什么需要映射,以及除了这些绝对需求之外,它们还支持哪些特性。它讨论了各种类型的关联,以及在什么情况下您会选择使用它们。下一章着眼于如何指定映射。
六、使用注解的映射
在第五章中,我们讨论了在数据库模型和对象模型之间创建映射的需要。可以通过两种不同的方式创建映射:通过内联注解(正如我们到目前为止在本书中所做的那样),或者作为两种主要格式之一的单独的 XML 文件(Hibernate 的内部 XML 格式和 JPA 的映射格式,这两种格式都有价值,但不建议用于大多数应用)。
基于 XML 的映射很少在需要将对象模型映射到预先存在的模式的情况之外使用;即使这样,对注解的熟练使用也可以匹配 XML 配置的特性。
使用注解创建 Hibernate 映射
在内联注解出现之前,创建映射的唯一方式是通过 XML 文件——尽管 Hibernate 和第三方项目的工具 1 允许部分或全部从 Java 源代码中生成。尽管使用注解是定义映射的最新方法,但它并不是最好的方法。在讨论何时以及如何应用注解之前,我们将简要讨论注解的缺点和优点。
注解的缺点
如果您从早期的 Hibernate 环境升级,您可能已经有了基于 XML 的映射文件来支持您的代码库。在其他条件相同的情况下,您不会希望仅仅为了重新表达这些映射而使用注解。
如果您正在从遗留环境中迁移,您可能不希望改变预先存在的 POJO 源代码,以免用可能的 bug 污染已知的好代码。 2 注解毕竟被编译到类文件中,因此可能被认为是对源代码或交付的工件的更改。
如果您没有 POJOs 的源代码(因为它是由自动化工具或类似工具生成的),那么您可能更喜欢使用外部的基于 XML 的映射来反编译类文件,以获得用于修改的 Java 源代码。
将映射信息作为外部 XML 文件进行维护,可以修改映射信息以反映业务变化或模式变更,而不必强制您重新构建整个应用。然而,当您有一个构建系统(Maven 或 Gradle,以及您可能有的任何持续集成工具)时,构建一个应用通常是非常容易的,所以这无论如何都不是一个令人信服的论点。
注解的好处
考虑到缺点,使用注解有一些强大的好处。
首先,也许是最有说服力的一点,我们发现基于注解的映射比基于 XML 的映射更直观,因为它们与相关的属性一起直接出现在源代码中。大多数编码人员倾向于使用注解,因为需要保持相互同步的文件更少。
部分由于这个原因,注解比它们的 XML 对等物更少冗长,清单 6-1 和 6-2 之间的对比证明了这一点,两者都会在数据库中创建一个Sample实体。这里的 XML 直接映射到正在使用的注解。
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE
hibernate-mapping
PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping default-access="field">
<class name="Sample">
<id type="int" column="id">
<generator class="native"/>
</id>
<property name="name" type="string"/>
</class>
</hibernate-mapping>
Listing 6-2An Equivalent Mapping with XML
import javax.persistence.* ;
@Entity
public class Sample {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer id;
public String name;
}
Listing 6-1A Minimal Class Mapped with Annotations
后一个清单中的一些冗长是 XML 本身的特性(标记名和样板文档类型声明),还有一些是由于注解与源代码的紧密集成。例如,在这里,XML 文件必须显式声明使用字段访问来代替属性访问(即,直接访问字段,而不是通过它们的 get/set 方法);但是注解从它被应用于id字段而不是getId()方法的事实中推断出这一点。
Hibernate 使用并支持 JPA 2 持久化注解。如果您选择在代码和注解中不使用特定于 Hibernate 的特性,那么您可以自由地使用其他支持 JPA 2 的 ORM 工具将您的实体部署到环境中。
最后——也许是次要的一点——因为注解被直接编译到适当的类文件中,所以缺少或陈旧的映射文件在部署时引起问题的风险更小(这一点可能对那些已经对 XML 技术的这种危险有一些经验的人最有说服力)。
选择要使用的映射机制
一般来说,更喜欢注解;注解本身在 JPA 实现中是可移植的,这是众所周知的。工具可以直接从数据库中创建带注解的源代码,因此,即使使用预先存在的模式,同步也不是什么大问题。
XML 映射可以用 Hibernate 的专有格式或 JPA 的标准 XML 配置来完成,这两种格式类似但不完全相同;如果您发现 XML 是一种更好的配置格式,那么最好使用行业标准 JPA 配置中的 XML 格式。 3
JPA 2 持久化注解
当您使用注解进行开发时,您从一个 Java 类开始,然后用元数据符号注解源代码清单。Hibernate 在运行时使用反射来读取注解并应用映射信息。如果您想使用 Hibernate 工具来生成数据库模式,您必须首先编译包含注解的实体类。在这一节中,我们将介绍 JPA 2 注解的重要核心,以及一组简单的类来说明它们是如何应用的。
最常见的注解是@Entity、@Id和@Column,仅供参考;其他常见的还有@GenerationStrategy(与@Id关联)以及@OneToOne、@ManyToOne、@OneToMany、@ManyToMany等与关联相关的注解。
示例类集代表出版商的图书目录。您从一个单独的类Book开始,它没有注解或映射信息。我们还将添加Author作为一个实体。就本例而言,您没有现有的数据库模式可以使用,因此您需要在使用过程中定义您的关系数据库模式。
这个Book类非常简单,就像我们开始时一样。它有两个字段,title和pages,以及一个标识符,id,它是一个整数。标题是一个String对象,pages是一个整数。在这个例子中,我们将向Book类添加注解、字段和方法。本章末尾给出了Book和Author类的完整源代码清单;其余部分的源文件可以在 Apress 网站上本章的源代码下载中获得( www.apress.com )。
清单 6-3 以未标注的形式给出了 Book 类源代码的基本外壳,作为该示例的起点。
package chapter06.primarykey.before;
public class Book {
String title;
int pages;
int id;
public Book() {
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
Listing 6-3src/main/java/chapter06/primarykey/before/Book.java
正如你所看到的,这是一个 POJO,尽管没有一些我们可能希望它拥有的东西,比如equals()、hashCode()和toString()。我们将继续注解这个类,解释注解背后的概念。最后,我们将把它转移到一个不同的包中,这样我们就可以对实体应该是什么样的有一个好的前后对比图。
带有@Entity的实体 Beans
第一步是将Book类注解为 JPA 2 实体 bean。我们将@Entity注解添加到Book类中,如下所示:
import javax.persistence.Entity;
@Entity
public class Book
// Declaration of instance variables goes here
public Book() {
}
// .. the rest of the class
这些源列表是“正在进行中的”,所以它们在书的源代码中没有单独的文件。该类的实际源代码将与我们在本章中开发的代码不同。我们也将在本章的后面看到“最终产品”。
JPA 2 标准注解包含在javax.persistence包中,所以我们导入适当的注解。一些 ide 将使用特定的导入,而不是“星形导入”,在这种情况下,一个给定的包中的几个类被导入;通常,一个实体会使用这个包中的相当多的注解,所以在任何情况下都有可能以 star imports 结束。4
@Entity注解将该类标记为实体 bean,因此它必须有一个至少在protected范围内可见的无参数构造函数。 5 Hibernate 最少支持包范围,但是如果你利用了这一点,你就失去了对其他 JPA 实现的可移植性。实体 bean 类的其他 JPA 2 规则是:( a)该类不能是最终的,以及(b)实体 bean 类必须是具体的。JPA 2 实体 bean 类和 Hibernate 持久化对象的许多规则是相同的——一部分是因为 Hibernate 团队在 JPA 2 设计过程中投入了很多,另一部分是因为设计相对不引人注目的对象关系持久化解决方案的方法就这么多。
到目前为止,我们已经添加了实体注解、构造函数和导入语句。POJO 的其余部分被单独留下。
带有@Id和@GeneratedValue的主键
每个实体 bean 都必须有一个主键,您可以用@Id注解在类上对其进行注解。通常,主键是单个字段,尽管它也可以是多个字段的组合。
@Id注解的位置决定了 Hibernate 用于映射的默认访问策略。如果注解应用于一个字段,如我们的下一个代码片段所示,那么字段访问将通过反射使用:
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Sample {
@Id
int id;
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = id;
}
// .. the rest of the class
}
相反,如果注解应用于字段的访问器,如我们的下一个代码片段所示,那么将使用属性访问。属性访问意味着 Hibernate 会调用 mutator,而不是实际直接设置字段;这也意味着 mutator 可以在设置值时改变值,或者改变对象中其他可用的状态。选择哪一个取决于你的喜好和需要;通常实地访问就足够了。 6
为什么不总是使用字段访问呢?考虑一下美国的社会安全号码:它们实际上是三组信息——一个三位数的区号;一个群号,是两位数;还有一个的序列号。虽然这些数字没有绝对的意义,但是您可能希望将它们分隔到一个类的不同属性中,即使它作为单个元素存储在数据库中;通过属性访问,您可以使用一种方法将程序的三个元素分开,同时保持数据库结构不变。
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Sample {
int id;
// note that we've moved the @Id annotation to the accessor
@Id
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = id;
}
// .. the rest of the class
}
在这里,您可以看到注解方法的优势之一。因为注解与源代码内嵌在一起,所以可以从代码中映射的上下文中提取信息,从而可以推断出许多映射决策,而不是显式地声明,这有助于进一步减少注解的冗长性。
默认情况下,@Id注解不会创建主键生成策略, 7 ,这意味着作为代码的作者,您需要确定什么是有效的主键。您可以通过使用@GeneratedValue注解让 Hibernate 为您确定主键。这需要一对属性:strategy和generator。
strategy属性必须是来自javax.persistence.GenerationType枚举的值。如果不指定生成器类型,默认为AUTO。GenerationType上有四种不同类型的主键生成器,如表 6-1 所示。
表 6-1
GenerationType选项
战略
|
描述
|
| --- | --- |
| AUTO | Hibernate 根据数据库对主键生成的支持来决定使用哪种生成器类型。 |
| IDENTITY | 数据库负责确定和分配下一个主键。不建议这样做,因为它对事务和批处理有影响。 |
| SEQUENCE | 一些数据库支持SEQUENCE列类型。请参阅本章后面的“用@SequenceGenerator生成主键值”一节。 |
| TABLE | 这种类型用主键值保存一个单独的表。请参阅本章后面的“用@TableGenerator生成主键值”一节。 |
generator 属性允许使用自定义生成机制。Hibernate 为四种策略中的每一种都提供了命名生成器,还有其他的,比如“hilo、“uuid”和“guid”如果您需要使用 Hibernate 特有的主键生成器,比如hilo,那么您的应用可能会丧失在其他 JPA 2 环境中的可移植性;也就是说,Hibernate 生成器提供了更多的灵活性和控制。
对于 Book 类,我们将使用AUTO密钥生成策略。让 Hibernate 决定使用哪种类型的生成器可以让您的代码在不同的数据库之间移植。
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
int id;
用@SequenceGenerator 生成主键值
正如在@Id标签中提到的,我们可以声明主键属性是由数据库序列生成的。序列是一种数据库对象,可用作主键值的来源。它类似于标识列类型的使用,只是序列独立于任何特定的表,因此可以由多个表使用。
序列表用于多个标识符的能力有用吗?看情况。序列生成器在大量访问的情况下处于最佳状态,因为它以块为单位分配标识符,所以在分配新的键时不会发生冲突,所以在许多情况下这是一个很好的选择。然而,这并不能保证你会得到一系列可预测的标识符,因为一旦分配了一个块,这个数字块就不能再用于任何其他标识符的生成。
要声明要使用的特定序列对象及其属性,必须在带注解的字段中包含@SequenceGenerator注解。这里有一个例子:
@Id
@SequenceGenerator(name="seq1",sequenceName="HIB_SEQ")
@GeneratedValue(strategy=SEQUENCE,generator="seq1")
int id;
这里声明了一个名为seq1的序列生成注解。这指的是名为HIB_SEQ的数据库序列对象。然后名称seq1被引用为@GeneratedValue注解的generator属性。
只有序列生成器名称是必需的;其他属性将采用合理的默认值,但是作为一种良好的实践,您应该为sequenceName属性提供一个显式值。如果没有指定,要使用的sequenceName值由持久化提供者(在本例中是 Hibernate)选择。其他(可选)属性为initialValue(生成器以此编号启动)和allocationSize(一次预留的序列中 id 的数量);initialValue默认为 1,新分配的块大小默认为 50,这对大多数应用来说都很好。如果您的应用最终在几毫秒内生成超过 50 个标识符,使用更大的分配大小可能不是一个坏主意,因为 Hibernate 会在需要更多标识符时生成一个查询来获取新的块。
用@TableGenerator 生成主键值
@TableGenerator注解的使用方式与@SequenceGenerator注解非常相似,但是因为@TableGenerator操作一个标准的数据库表来获得它的主键值,而不是使用一个特定于供应商的序列对象,所以它保证了在数据库平台之间的可移植性。
为了获得最佳的可移植性和最佳的性能,您不应该指定使用表生成器,而应该使用@GeneratorValue(strategy=GeneratorType.AUTO)配置,它允许持久化提供者为正在使用的数据库选择最合适的策略。
与序列生成器一样,@TableGenerator的名称属性是必需的,其他属性是可选的,表的详细信息由持久化提供者选择:
@Id
@TableGenerator(name="tablegen",
table="ID_TABLE",
pkColumnName="ID",
valueColumnName="NEXT_ID")
@GeneratedValue(strategy=TABLE,generator="tablegen")
int id;
可选属性如表 6-2 所示。
表 6-2
@TableGenerator可选属性
属性名
|
意义
|
| --- | --- |
| allocationSize | 允许根据性能调整一次留出的主键数量。默认为 50。当 Hibernate 需要分配一个主键时,它会从键表中抓取一个键“块”,并按顺序分配键,直到这个块被使用,所以它会在每次allocationSize分配时更新这个块。 |
| catalog | 允许指定表所在的数据库目录。 |
| indexes | 这是一个javax.persistence.Index注解列表,表示表的显式索引,这些索引不能从@Column说明符派生,通常是复合索引。 |
| initialValue | 允许指定起始主键值。默认为 1。 |
| pkColumnName | 允许标识表的主键列。该表可以包含为多个实体生成主键值所需的详细信息。 |
| pkColumnValue | 允许标识包含主键生成信息的行的主键。 |
| schema | 允许指定表所在的模式。 |
| table | 包含主键值的表的名称。 |
| uniqueConstraints | 允许将附加约束应用于表以生成架构。 |
| valueColumnName | 允许标识包含当前实体的主键生成信息的列。 |
因为该表可用于包含各种条目的主键值,所以每个使用它的实体可能都有一个单独的行。因此,它需要自己的主键(pkColumnName),以及一个包含下一个主键值的列(pkColumnValue),用于任何从它那里获取主键的实体。
用@Id、@IdClass 或@EmbeddedId 组合主键
尽管出于各种原因,使用单列代理键是有利的,但有时您可能会被迫使用业务键。当这些包含在一个单独的列中时,您可以使用@Id而无需指定生成策略(强制用户在实体被持久化之前分配一个主键值)。但是,当主键由多个列组成时,您需要采用不同的策略将这些列组合在一起,以允许持久化引擎将键值作为单个对象进行操作。
您必须创建一个类来表示这个主键。当然,它不需要自己的主键,但是它必须是一个公共类,必须有一个默认的构造函数,必须是可序列化的,并且必须实现hashCode()和equals()方法,以允许 Hibernate 代码测试主键冲突(也就是说,它们必须用主键值的适当的数据库语义来实现)。
一旦创建了主键类,使用它的三种策略如下:
-
将其标记为
@Embeddable,并将其添加到您的实体类中,就像它是一个普通属性一样,标记为@Id。 -
将它添加到您的实体类中,就像它是一个普通属性一样,用
@EmbeddableId标记。 -
为实体类的所有字段添加属性,用
@Id标记它们,用@IdClass标记实体类,提供主键类的类。
所有这些技术都需要使用一个id类,因为在调用 Hibernate 的持久化 API 的各个部分时,必须为 Hibernate 提供一个主键对象。例如,您可以通过调用Session对象的get()方法来检索实体的实例,该方法将表示实体主键的单个可序列化对象作为其参数。
对标记为@Embeddable的类使用@Id,如我们接下来的清单所示,是最自然的方法。无论如何,@Embeddable注解可以用于非主键可嵌入值(@Embeddable将在本章后面详细讨论)。它允许您将复合主键视为单个属性,并允许在其他表中重用@Embeddable类。
嵌入的主键类必须是可序列化的(即,它们必须实现java.io.Serializable,尽管也可以使用java.io.Externalizable8)。
首先,让我们看一下密钥本身,一个 ISBN,或国际标准书号。ISBNs 作为书籍的自然唯一标识符是有意义的,因为这就是它们的真正用途;它们在所有文学作品中都是独一无二的,因此每一种印刷和封面类型的书都有自己的 ISBN。(换句话说,不仅每本书的不同版本都有自己的 ISBN,你正在阅读的书的电子版和平装版都有自己独特的 ISBN。)
package chapter06.compoundpk;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Embeddable
public class ISBN implements Serializable {
@Column(name = "group_number")
int group;
int publisher;
int title;
int checkDigit;
public ISBN() {
}
public int getGroup() {
return group;
}
public void setGroup(int group) {
this.group = group;
}
public int getPublisher() {
return publisher;
}
public void setPublisher(int publisher) {
this.publisher = publisher;
}
public int getTitle() {
return title;
}
public void setTitle(int title) {
this.title = title;
}
public int getCheckDigit() {
return checkDigit;
}
public void setCheckDigit(int checkdigit) {
this.checkDigit = checkdigit;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ISBN)) return false;
ISBN isbn = (ISBN) o;
if (checkDigit != isbn.checkDigit) return false;
if (group != isbn.group) return false;
if (publisher != isbn.publisher) return false;
if (title != isbn.title) return false;
return true;
}
@Override
public int hashCode() {
int result = group;
result = 31 * result + publisher;
result = 31 * result + title;
result = 31 * result + checkDigit;
return result;
}
}
Listing 6-4src/main/java/chapter06/compoundpk/ISBN.java
这里的@Embeddable注解意味着,确切地说,它应该作为一个实体嵌入到包含类中;对于ISBN,group,publisher,title和checkDigit都作为包含类的列导出。
group字段实际上对数据库列使用了不同的名称,因为group是 SQL 中的保留字。这里,我们更喜欢我们自己到group_number的映射,而不是数据库方言可能喜欢的任何映射。
使用这个可嵌入键CPKBook——其中CPK表示“复合主键”——的类看起来可能如清单 6-5 所示。
package chapter06.compoundpk;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class CPKBook {
@Id
ISBN id;
@Column
String name;
public CPKBook() {
}
public ISBN getId() {
return id;
}
public void setId(ISBN id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String title) {
this.name = title;
}
}
Listing 6-5src/main/java/chapter06/compoundpk/CPKBook.java
当然,作为一个简单的例子,这个Book非常稀疏。但是假设它有一个可嵌入的标识符类——即ISBN——创建cpkbook表的 SQL 将如清单 6-6 所示。
create table CPKBook (
checkDigit integer not null,
group_number integer not null,
publisher integer not null,
title integer not null,
name varchar(255),
primary key (checkDigit, group_number, publisher, title)
);
Listing 6-6The Generated DDL for CPKBook
我们的下一个例子使用了一个@EmbeddedId注解来创建与清单 6-6 中完全相同的 DDL,除了表名。嵌入式 id 的主要区别在于嵌入式密钥的可访问性和范围;在这里,我们基本上将EmbeddedISBN描述为一个EmbeddedPKBook的范围中的一个类。我们这里的例子是人为设计的,因为 ISBN 有一个普遍接受的含义(当你看到“ISBN”时,你通常会想到书,而不是想知道这个术语来自哪个领域),但是如果你对关键字有一个限定的含义,你就可以使用这个。
清单 6-7 非常短,主要是因为EmbeddedISBN和我们标记为@Embeddable的ISBN有着完全相同的类定义。
package chapter06.compoundpk;
import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import java.io.Serializable;
@Entity
public class EmbeddedPKBook {
@EmbeddedId
EmbeddedISBN id;
@Column
String name;
static class EmbeddedISBN implements Serializable {
// source matches the listing for ISBN.java
}
}
Listing 6-7src/main/java/chapter06/compoundpk/EmbeddedPKBook.java
我们的最后一个例子使用了@IdClass,它与@EmbeddedId的例子非常相似。有了@IdClass,实体就有了匹配id 类定义的字段,这些字段在实体中都标有@Id;key 类用于像get()和load()这样的Session方法,这些方法需要单个对象通过键来查找类。
这个类的 DDL 看起来与CPKBook和EmbeddedPKBook完全相同,除了表名。
package chapter06.compoundpk;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import java.io.Serializable;
@Entity
@IdClass(IdClassBook.EmbeddedISBN.class)
public class IdClassBook {
@Id
@Column(name = "group_number")
int group;
@Id
int publisher;
@Id
int title;
@Id
int checkdigit;
String name;
public IdClassBook() {
}
static class EmbeddedISBN implements Serializable {
int group;
int publisher;
int title;
int checkdigit;
public EmbeddedISBN() {
}
// source matches the listing for ISBN.java
}
}
Listing 6-8src/main/java/chapter06/compoundpk/IdClassBook.java
@IdClass注解使用了EmbeddedISBN类的完全作用域名称,其简单名称与来自EmbeddedPKBook的EmbeddedISBN类相匹配。这是一个正在讨论的类名作用域的例子;键类的完全限定名是不同的,即使简单名不是。
关键类字段必须与它们所应用的实体相匹配;这里不能使用不同的名称或类型。(然而,这些字段的顺序并不相关。)实体类的字段用@Id标记,这在创建复合主键时感觉有些自然。
那么,当有更多的规则适用于使用 id 类时,为什么还要使用它呢?主要是自然物体参照物。id 类的存在不会影响实体定义,因为像publisher这样的字段是包含实体的顶级属性,而不是 ISBN 的一部分。是否即是你的对象模型的一个想要的属性取决于你。 9
对于哪一个是“最好的”,有什么偏好吗简短的回答是“是”,真正的回答是“否”。根据轶事经验,@Embeddable方法感觉最自然,但是每个应用和问题领域的特定需求确实有助于确定您的对象模型;很容易让键由与@IdClass的外键关系组成,并且@IdClass和@EmbeddedId都允许您限制键类型的有效可见性,因为当您想要一个 ISBN 用作主键而时,您有另一个东西,您可以设法调用一个 ISBN 类型,它在同一个包中有其他用途或含义。
永远做最适合你的代码的事情。
用@Table和@SecondaryTable映射数据库表
默认情况下,表名源自实体名。因此,给定一个带有简单的@Entity注解的类Book,表名将是“book”,根据数据库的配置进行调整。
如果实体名称由于某种原因被更改(通过在@Entity注解中提供一个不同的名称,比如@Entity("BookThing"),新名称将被用于实体名称。(查询需要使用实体名称;从用户的角度来看,表名是不相关的。)
可以进一步定制表名,并且可以通过@Table注解配置其他与数据库相关的属性。该注解允许您指定表的许多细节,这些细节将用于在数据库中持久存储实体。正如已经指出的,如果您省略注解,Hibernate 将默认使用类名作为表名,由数据库方言规范化,因此如果您想要覆盖该行为,只需要提供这个注解。
@Table注解提供了四个属性,允许您覆盖表的名称、目录和模式,并对表中的列实施惟一约束。通常,您只需提供一个替代表名,即@Table(name="ORDER_HISTORY")。如果数据库模式是从带注解的类生成的,那么唯一约束将被应用,并将补充任何特定于列的约束(参见本章后面对@Column和@JoinColumn的讨论)。它们不会被强制执行。
@SecondaryTable注解提供了一种对跨几个不同数据库表持久化的实体 bean 建模的方法。 10 这里,除了为主数据库表提供一个@Table注解外,您的实体可以有一个@SecondaryTable注解,或者一个@SecondaryTables注解,依次包含零个或多个@SecondaryTable注解。@SecondaryTable注解采用与@Table注解相同的基本属性,并增加了pkJoinColumns属性。pkJoinColumns属性定义了主数据库表的连接列。它接受一组javax.persistence.PrimaryKeyJoinColumn对象。如果您省略了pkJoinColumns属性,那么将假设这些表是在同名的主键列上连接的。
当从二级表中提取实体中的属性时,必须用@Column标注进行标记,用表属性标识相应的表;否则,它将从“主”数据库表中取出。清单 6-9 展示了如何从以这种方式映射的第二个表中提取一个Customer实体的属性。
package chapter06.twotables;
import javax.persistence.*;
@Entity
@Table(
name = "customer",
uniqueConstraints = {@UniqueConstraint(columnNames = "name")}
)
@SecondaryTable(name = "customer_details")
public class Customer {
@Id
public int id;
public String name;
@Column(table = "customer_details")
public String address;
public Customer() {
}
}
Listing 6-9src/main/java/chapter06/twotables/Customer.java
该类可以在 H2 数据库中使用以下 SQL 建模:
create table customer
(id integer not null, name varchar(255), primary key (id));
create table customer_details
(address varchar(255), id integer not null, primary key (id));
alter table if exists customer
add constraint UKcrkjmjk1oj8gb6j6t5kt7gcxm unique (name);
alter table if exists customer_details
add constraint FK4g7jhj0n6g33lh0ar8ii6c9to foreign key (id) references customer;
通过向@Table或@SecondaryTable的uniqueConstraints属性添加一个或多个适当的@UniqueConstraint注解,可以将主表或辅助表中的列标记为在它们的表中具有唯一值,如name所示。您也可以使用@Column属性上的唯一属性在字段级别设置唯一性。
用@Basic持久化基本类型
默认情况下,POJO 中的属性和实例变量是持久的;Hibernate 会为你存储它们的值。因此,最简单的映射是针对“基本”类型的。这些包括基元、基元包装器、基元或包装器的数组、枚举以及任何实现Serializable但本身不是映射实体的类型。这些都是隐式映射的——不需要注解。默认情况下,这些字段被映射到一个单独的列,并使用渴望获取来检索它们(即,当从数据库中检索实体时,所有的基本字段和属性都被检索 11 )。此外,当字段或属性不是原语时,它可以作为一个null值存储和检索。
通过对适当的类成员应用@Basic注解,可以覆盖这个默认行为。该注解带有两个可选属性,并且本身是完全可选的。第一个属性被命名为optional,并带有一个Boolean。默认设置为true,可以设置为false来提示模式生成(或者模式使用,如果你不让 Hibernate 管理模式的话)应该创建相关的列NOT NULL。 12 第二个被命名为fetch并接受一个枚举成员FetchType。默认情况下为EAGER,但可以设置为LAZY以允许在访问该值时加载。
惰性加载意味着当对象被Session加载时,引用的值实际上不一定被初始化。这具有潜在的性能优势——您可以查看对象是否被持久化,而不必设置其所有属性——但这也意味着您的对象可能在任何给定时刻都没有完全初始化。(当你真正开始访问数据时,它被初始化。)这意味着对于延迟加载的数据,当您访问数据时,发起的Session必须是活动的,否则您将得到一个LazyInitializationException异常。
当您真正从数据库加载关系时,惰性初始化最有价值。如果你有一个PublishingHouse对象,已经出版了成千上万本书,你不需要仅仅因为你正在使用PublishingHouse引用就加载所有的书。因此,您可能希望书籍被延迟加载(因为它们可能有自己对Author对象的引用,并且那些作者可能有多本书,等等,如此循环往复)。
通常省略@Basic属性,使用@Column注解的nullable属性,否则@Basic注解的可选属性可能用于提供NOT NULL行为。
用@Transient 省略持久化
一些字段,如计算值,可能只在运行时使用,当实体保存到数据库中时,它们应该从对象中丢弃。JPA 规范为这些瞬态字段提供了@Transient注解。@Transient 注解没有任何属性——您只需根据实体 bean 的属性访问策略将它添加到实例变量或 getter 方法中。
@Transient注解强调了在 Hibernate 中使用注解和使用 XML 映射文档之间的一个重要区别。有了注解,Hibernate 将默认保存映射对象上的所有字段。当使用 XML 映射文档时,Hibernate 要求您明确地告诉它哪些字段将被持久化。 13
对于我们的例子,如果我们想要添加一个名为publicationDate的Date字段,而不是存储在我们的 Book 类的数据库中,我们可以这样标记这个字段:
@Transient
Date publicationDate;
如果我们对我们的Book类使用属性访问策略,我们将需要在访问器上放置@Transient注解。
用@Column 映射属性和字段
@Column注解用于指定字段或属性将被映射到的列的细节。有些细节是与模式相关的,因此只有当模式是从带注解的文件生成时才适用。其他的由 Hibernate(或者 JPA 2 持久化引擎)在运行时应用和执行。它是可选的,具有一组适当的默认行为,但在覆盖默认行为或需要将对象模型放入预先存在的模式中时通常很有用。它比类似的@Basic注解更常用,表 6-3 中的属性通常被覆盖。
表 6-3
@Column属性
属性
|
描述
|
| --- | --- |
| name | 这允许显式指定列的名称——默认情况下,这将是属性的名称。但是,如果默认行为会导致 SQL 关键字被用作列名(例如,user或group,两者都是 SQL 关键字,那么通常有必要覆盖默认行为;例如,你可以用user_name代替user。 |
| length | length允许显式定义用于映射值(尤其是String值)的列的大小。列大小默认为 255,否则可能会导致截断的String数据。 |
| nullable | 这控制了列的可空性。如果 schema 是 Hibernate 生成的,那么该列会被标记为NOT NULL;否则,此处的值会影响对象的验证。默认情况下,应该允许字段为null;然而,当一个字段是或者应该是强制的时,通常会覆盖它。 |
| unique | 这将标记只包含唯一值的字段。这默认为 false,但通常会为一个可能不是主键但如果重复仍会导致问题的值设置(如 username)。如果 Hibernate 不管理模式,这几乎没有影响。 |
| table | 当所属实体已经跨一个或多个辅助表映射时,使用该属性。默认情况下,假设该值来自主表,但是在这里可以替换其中一个辅助表的名称(参见本章前面的@SecondaryTable注解示例)。 |
| insertable | 这个值控制 Hibernate 是否会为这个字段创建值。它默认为 true,但是如果设置为 false,Hibernate 生成的 insert 语句将省略注解字段(例如,Hibernate 最初不会保存它,但是可能会更新它;参见下面的updatable属性。) |
| updatable | 这默认为 true,但是如果设置为 false,Hibernate 生成的 update 语句将省略注解字段(也就是说,一旦它被持久化,就不会被更改)。 |
| columnDefinition | 该值可以设置为在数据库中生成列时使用的适当的 DDL 片段。这只能在从带注解的实体生成模式的过程中使用,如果可能的话应该避免,因为这可能会降低应用在数据库方言之间的可移植性。 |
| precision | precision允许为模式生成指定十进制数字列的精度,并且在保留非十进制值时将被忽略。给定的值代表数字中的位数(通常要求最小长度为n+1,其中n是刻度,见下表)。 |
| scale | 这允许为模式生成指定十进制数字列的小数位数,并且在保留非十进制值的情况下将被忽略。给定的值代表小数点后的位数。 |
下面是一个(简短的)例子,说明如何将这些属性应用到一个Book类的title字段:
@Column(name="working_title",length=200,nullable=false)
String title;
同样,对于一个表示十进制数的类,您可能会得到如下结果:
@Column(scale=2,precision=5,nullable=false)
double royalty;
如果数据库支持十进制值的微调精度,这将把类似于10.2385的数字作为10.24保存。这里的推论相当明显:您的数据库可能不支持这个特性。例如,即使设置了precision和scale,H2 也会为royalty存储一个全浮点值。
建模实体关系
自然,注解也允许您对实体之间的关联进行建模。JPA 2 支持一对一、一对多、多对一和多对多的关联。每一个都有相应的注解。
我们在第五章的表格中讨论了建立这些映射的各种方法。在这一节中,我们将展示如何使用注解来请求各种映射。
映射嵌入式(组件)一对一关联
当一个实体的所有字段都与另一个实体维护在同一个表中时,被包含的实体在 Hibernate 中被称为组件。JPA 标准将这种实体称为嵌入式。
这也适用于 id 类,并且使用的命名非常相似。
@Embedded和@Embeddable属性用于管理这种关系。在本章的主键示例中,我们以这种方式将一个ISBN类与一个Book类相关联。
ISBN类标有@Embeddable注解。一个可嵌入的实体必须完全由基本字段和属性组成。一个可嵌入的实体只能使用@Basic、@Column、@Lob、@Temporal和@Enumerated注解。它不能用@Id注解维护自己的主键,因为它的主键是封闭实体的主键。
@Embeddable注解本身纯粹是一个标记注解,它没有附加属性,如下所示。通常,可嵌入实体的字段和属性不需要进一步的标记。
@Embeddable
public class AuthorAddress {
...
}
然后,封闭实体在实体中标记适当的字段或 getters,利用带有@Embedded注解的可嵌入类,如下所示:
@Embedded
AuthorAddress address;
@Embedded注解从嵌入类型中提取它的列信息,但是允许用@AttributeOverride和@AttributeOverrides注解覆盖特定的一列或多列(如果多个列被覆盖,后者包含前者的一个数组)。例如,这里我们看到如何用名为ADDR和NATION的列覆盖默认的列名AuthorAddress的 address 和 country 属性,这两个名称肯定是由稍有恶意的数据库分析师选择的:
@Embedded
@AttributeOverrides({
@AttributeOverride(name="address",column=@Column(name="ADDR")),
@AttributeOverride(name="country",column=@Column(name="NATION"))
})
AuthorAddress address;
Hibernate 和 JPA 标准都不支持跨多个表映射一个嵌入对象。在实践中,如果您希望您的嵌入式实体具有这种持久化,您通常会更好地让它成为一个一级实体(即,非嵌入式的),拥有自己的@Entity标记和@Id注解,然后通过传统的一对一关联对其进行映射,这将在下一节中解释。 14
映射传统的一对一关联
如果一个实体不是另一个实体的组件(即嵌入到另一个实体中),那么在两个实体之间映射一对一的关联本质上没有任何问题。然而,这种关系往往有些可疑。在使用@OneToOne注解之前,您应该考虑使用前面描述的嵌入技术。
那你为什么想要一对一的联系呢?好吧,考虑这样一种情况,你有密切相关的实体,但仍然保留被独立访问的能力:一个Home,可能有一个地址,一个Homeowner,实体的名字在那个Home的契约上。
您可以通过一对一的关联建立双向关系。一方需要拥有关系,并负责用另一方的外键更新连接列。非拥有方需要使用 mappedBy 属性来指示拥有该关系的实体。
假设您决心以这种方式声明关联(可能是因为您期望在可预见的将来将其转换为一对多或多对一的关系),那么应用注解就非常简单——所有的属性都是可选的。下面是声明这种关系的简单方式:
@OneToOne
Address address;
@OneToOne注解允许指定可选属性(表 6-4 )。
表 6-4
OneToOne标注属性
属性
|
描述
|
| --- | --- |
| targetEntity | 这可以设置为存储关联的实体的类。如果未设置,将从字段类型或属性 getter 的返回类型中推断出适当的类型。 |
| cascade | 这个值可以设置为javax.persistence.CascadeType枚举的任何成员。默认设置为无。有关这些值的讨论,请参见“级联操作”一节。 |
| fetch | 这可以设置为FetchType的EAGER或LAZY成员。(默认为EAGER。) |
| optional | 这表示正在映射的值是否可以是null。 |
| orphanRemoval | 此属性表明,如果被映射的值被删除,此实体也将被删除。 |
| mappedBy | 该值指示双向一对一关系由命名实体拥有。 15 所属实体包含下属实体的主键。 |
映射多对一或一对多的关联
多对一关联和一对多关联分别从拥有实体和从属实体的角度来看是相同的关联。
维护两个实体之间多对一关系的最简单方法是,将一对多关系中“一”端实体的外键作为“多”实体表中的一列进行管理。
@OneToMany注解可以应用于代表关联的映射“多”端的集合或数组的字段或属性值:
@OneToMany(cascade = ALL,mappedBy = "publisher")
Set<Book> books;
这种关系的多对一端用类似于一对多端的术语来表示,如下所示:
@ManyToOne
@JoinColumn(name = "publisher_id")
Publisher publisher;
@ManyToOne注解采用了一组与@OneToMany相似的属性。表 6-5 中的列表描述了这些属性,这些属性都是可选的。
表 6-5
@ManyToOne属性
属性
|
描述
|
| --- | --- |
| cascade | 这表示对关联操作的适当级联策略;默认为无。 |
| fetch | 此属性指示要使用的提取策略;默认为LAZY。 |
| optional | 这指示该值是否可以为空;默认为true。 |
| targetEntity | 该值指示存储主键的实体-这通常是从字段或属性的类型(在前面的示例中为 Publisher)中推断出来的。 |
我们还提供了可选的@JoinColumn属性来命名关联所需的外键列,而不是默认的(publisher)——这不是必需的,但它说明了注解的用法。(如果没有指定,Hibernate 将从“所属类型”派生一个外键列名。)
当要形成单向的一对多关联时,可以使用链接表来表达这种关系。这是通过添加@JoinTable注解实现的,如下图: 16
@OneToMany(cascade = ALL)
@JoinTable
Set<Book> books;
@JoinTable注解提供了允许控制链接表各个方面的属性。这些属性如表 6-6 所示。
表 6-6
@JoinTable属性
属性
|
描述
|
| --- | --- |
| name | 这是用于表示关联的连接表的名称。 |
| catalog | 这是包含连接表的目录的名称。 |
| schema | 这是包含连接表的模式的名称。 |
| joinColumns | 这个引用是一个由@JoinColumn属性组成的数组,代表关联“一”端实体的主键。如果“一”端有一个复合主键,您将使用多个值。 |
| inverseJoinColumns | 这是一个由@JoinColumn属性组成的数组,代表关联“多”端实体的主键。 |
在这里,我们看到了@JoinTable 注解的一个非常典型的应用,它将连接表的名称及其外键指定到关联的实体中:
@OneToMany(cascade = ALL)
@JoinTable(
name="PublishedBooks",
joinColumns = { @JoinColumn( name = "publisher_id") },
inverseJoinColumns = @JoinColumn( name = "book_id")
)
Set<Book> books;
映射多对多关联
当多对多关联不涉及连接关系双方的一级实体时,必须使用链接表来维护关系。这可以自动生成,也可以通过与本章前面的“映射多对一或一对多关联”一节中描述的链接表相同的方式来建立详细信息。
合适的注解自然是@ManyToMany,并采用表 6-7 中所示的属性。
表 6-7
@ManyToMany属性
属性
|
描述
|
| --- | --- |
| mappedBy | 这是指拥有关系的字段,只有在关联是双向的情况下才需要。如果一个实体提供了这个属性,那么关联的另一端就是这个关联的所有者,这个属性必须命名这个实体的一个字段或者属性。 |
| targetEntity | 这是作为关联目标的实体类。同样,这可以从泛型或数组声明中推断出来,只有在这种推断不可能时才需要指定。(如果 Hibernate 不能完全推断出模式,那么在模式生成时会出现错误。) |
| cascade | 这表示关联的级联行为,默认为无。 |
| fetch | 这表示关联的获取行为,默认为 LAZY。 |
这个例子维护了Book实体和Author实体之间的多对多关联。Book 实体拥有关联,因此它的getAuthors()方法必须用适当的@ManyToMany属性标记,如下所示:
@ManyToMany(cascade = ALL)
Set<Author> authors;
这里的Author实体是由即Book实体管理的。链接表不是显式管理的,因此,如下面的代码片段所示,我们用一个@ManyToMany注解对其进行标记,并指出外键由关联的Book实体的作者属性管理:
@ManyToMany(mappedBy = "authors")
Set<Book> books;
或者,我们可以指定完整的链接表:
@ManyToMany(cascade = ALL)
@JoinTable(
name="Books_to_Author",
joinColumns={@JoinColumn(name="book_ident")},
inverseJoinColumns={@JoinColumn(name="author_ident")}
)
Set<Author> authors;
级联操作
当建立两个实体之间的关联时(比如人和宠物之间的一对一关联,或者客户和订单之间的一对多关联),通常希望对一个实体的某些持久化操作也应用到它所链接的实体。以下面的代码为例:
Human dave = new Human("dave");
Pet cat = new PetCat("Tibbles");
dave.setPet(cat);
session.save(dave);
在最后一行,我们可能想要保存与人类对象相关联的Pet对象。在一对一的关系中,我们通常期望对拥有实体的所有操作通过——也就是说,级联到——依赖实体。在其他关联中,这是不正确的,即使在一对一的关系中,我们也可能有特殊的原因想要免除依赖实体的删除操作(可能是出于审计的原因)。
因此,我们能够使用cascade属性指定应该通过关联级联到另一个实体的操作类型,该属性接受一个由CascadeType枚举成员组成的数组。这些成员与用于 EJB 3 持久化的EntityManager类的关键方法的名称相对应,并与实体上的操作有如下粗略的对应关系:
-
ALL要求将所有操作级联到相关实体。这与包含MERGE、PERSIST、REFRESH、DETACH、REMOVE相同。 -
MERGE级联更新数据库中实体的状态(即UPDATE…)。 -
PERSIST级联数据库中实体状态的初始存储(即INSERT…)。 -
REFRESH从数据库级联更新实体的状态(即SELECT…)。 -
从托管持久化上下文中级联删除实体。
-
REMOVE从数据库中级联删除实体(即DELETE…)。 -
如果未指定级联类型,则不会通过关联级联任何操作。
根据这些选项,发布者与其地址之间关系的适当注解如下:
@OneToOne(cascade=CascadeType.ALL)
Address address;
集合排序
使用@OrderColumn注解来维护集合的顺序,可以在 Hibernate 或 JPA 2 中持久化有序集合。您还可以通过@OrderBy注解在检索时对集合进行排序。例如,如果您要检索一个按书籍名称升序排序的列表,您可以注解一个合适的方法。
以下代码片段指定了有序集合的检索顺序:
@OneToMany(cascade = ALL, mappedBy = "publisher")
@OrderBy("name ASC")
List<Book> books;
为什么books不是一个Set,既然你想在一个给定的收藏中只看一次给定的书?这是因为在 Java 中,Set是无序且唯一的,而List是有序的(且不是唯一的)。我们依赖于数据库的唯一性(通过假设,因为我们不能从这里的数据模型中判断),但是我们肯定想要有序的结果;Java 中当然有有序的Set类型,但是如果你想访问一个集合中的第五本书,比如说,你需要一个List。您选择哪种类型在很大程度上取决于您如何使用数据。
@OrderBy注解的值是作为排序依据的字段名的有序列表,每个字段名可选地附加 ASC(升序,如前面的代码所示)或 DESC(降序)。如果 ASC 或 DESC 都没有附加到其中一个字段名称,则顺序将默认为升序。@OrderBy可应用于任何集值关联。
遗产
JPA 2 标准和 Hibernate 都支持三种将继承层次映射到数据库的方法。这些措施如下:
-
单个表(
SINGLE_TABLE):每个类层次结构一个表 -
Joined (
JOINED):每个子类一个表(包括接口和抽象类) -
每个类一个表(
TABLE_PER_CLASS):每个具体的类实现一个表
通过继承相关的持久实体必须用@Inheritance 注解来标记。这需要一个策略属性,该属性被设置为对应于SINGLE_TABLE、JOINED或TABLE_PER_CLASS的三个javax.persistence.InheritanceType枚举值之一。
单一表格
单表方法为超类及其所有子类型管理一个数据库表。超类的每个映射字段或属性以及派生类型的每个不同字段或属性都有列。按照这种策略,当层次结构中的任何字段或属性名称发生冲突时,您需要确保适当地重命名列。
package chapter06.single;
import javax.persistence.Entity;
@Entity(name="SingleCBook")
public class ComputerBook extends Book {
String primaryLanguage;
}
Listing 6-11A Derived Entity in a SINGLE_TABLE Inheritance Tree
package chapter06.single;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
@Entity(name="SingleBook")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Book {
// contents common to all Books go here
@Id
Long bookId;
String title;
// imagine many more
}
Listing 6-10The Root of a SINGLE_TABLE Inheritance Tree
这个结构将创建一个名为SingleBook的表,包含以下字段:bookId、title、DTYPE和primaryLanguage,而primaryLanguage留在NULL处,用于存放不可分配给ComputerBook的书籍。这是 H2 数据库的样子:
create table SingleBook (
DTYPE varchar(31) not null,
bookId bigint not null,
title varchar(255),
primaryLanguage varchar(255),
primary key (bookId)
);
连接表
整体单表方法的一种替代方法是类似的连接表方法。这里使用了一个鉴别器列,但是各种派生类型的字段存储在不同的表中。(换句话说,你得到一个具有公共属性的“主表”,但是属于子类的属性得到它们自己的表。)除了不同的策略,这个继承类型以相同的方式指定(如清单 6-12 所示)。
package chapter06.joined;
import javax.persistence.Entity;
@Entity(name="JoinedCBook")
public class ComputerBook extends Book{
String primaryLanguage;
}
Listing 6-13A Leaf on a JOINED Inheritance Tree
package chapter06.joined;
import javax.persistence.*;
@Entity(name="JoinedBook")
@Inheritance(strategy = InheritanceType.JOINED)
public class Book {
// contents common to all Books go here
@Id
Long bookId;
String title;
// imagine many more
}
Listing 6-12The Root of a JOINED Inheritance Tree
使用 H2,可以通过下面的 SQL 看到这种结构:
create table JoinedBook
(bookId bigint not null, title varchar(255), primary key (bookId));
create table JoinedCBook
(primaryLanguage varchar(255), bookId bigint not null, primary key (bookId));
alter table if exists JoinedCBook
add constraint FK62rdg2vgeqlpviherbmj5b1su foreign key (bookId) references JoinedBook;
在这种情况下,如果我们假设ComputerBook看起来一样,我们有两个表:JoinedBook和JoinedCBook。JoinedBook表有bookId和title,而JoinedCBook表会有bookId(这就是它如何知道数据是如何关联的)和primaryLanguage。Hibernate 将查询这两个表,以确定检索时合适的实体类型。
每类表格
最后,还有每类一个表的方法,在这种方法中,继承层次结构中每种类型的所有字段都存储在不同的表中。由于实体和它的表之间的紧密对应关系,@DiscriminatorColumn注解不适用于这种继承策略。清单 6-14 展示了如何以这种方式映射我们的 Book 类。
package chapter06.perclass;
import javax.persistence.Entity;
@Entity(name="PerClassCBook")
public class ComputerBook extends Book {
String primaryLanguage;
}
Listing 6-15A Leaf on a TABLE_PER_CLASS Inheritance Tree
package chapter06.perclass;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
@Entity(name="PerClassBook")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Book {
// contents common to all Books go here
@Id
Long bookId;
String title;
// imagine many more
}
Listing 6-14The Root of a TABLE_PER_CLASS Inheritance Tree
这里我们(再次)有两个表,PerClassBook和PerClassCBook,但是PerClassBook有bookId和title,而PerClassCBook现在有三个列:bookId和title(就像PerClassBook)和primaryLanguage。在每个类一个表的策略中,每个实体都完全存储在自己的表中,与层次结构中的其他表没有数据库强制关系。
下面是为 H2 生成的 SQL:
create table PerClassBook (
bookId bigint not null,
title varchar(255),
primary key (bookId)
);
create table PerClassCBook (
bookId bigint not null,
title varchar(255),
primaryLanguage varchar(255),
primary key (bookId)
);
建模继承时在继承类型之间选择
这些不同的继承类型各有利弊。当创建模拟类层次结构的数据库模式时,必须权衡性能和数据库可维护性,以决定使用哪种继承类型。
当使用连接表方法时,维护数据库是最容易的。如果在类层次结构中的任何类中添加或删除字段,只需修改一个数据库表来反映这些变化。此外,向类层次结构添加新类只需要添加一个新表,消除了向大型数据集添加数据库列的性能问题。使用每类一个表的方法,对父类中列的更改要求在所有子表中进行列更改。单表方法可能会很混乱,导致表中的许多列并不是在每一行中都使用,以及一个快速水平增长的表。
单表方法的读取性能最好。对于层次结构中的任何类,select 查询将只从一个表中读取,不需要连接,只需要一个表。如果您只处理类层次结构中的叶节点(也就是说,如果您专门处理ComputerBook实体,而不是Book类型),则每类表类型具有很好的性能。任何与父类相关的查询都需要连接多个表才能得到结果。连接表方法还需要连接任何选择查询,因此这会影响性能。连接的数量与类层次结构的大小有关——大而深的类层次结构可能不适合连接表方法。
我们建议使用连接表方法,除非由于数据集的大小和类层次结构的深度而导致性能成为问题,但是这个决定完全基于您正在处理的数据的类型和数量。测量。
其他 JPA 2 持久化注解
虽然我们现在已经介绍了大多数核心 JPA 2 持久化注解,但是还有一些其他注解您会经常遇到。我们将在下面的章节中顺便介绍其中的一些。
时态数据
具有java.util.Date或java.util.Calendar类型的实体的字段或属性表示临时数据。默认情况下,这些将存储在数据类型为TIMESTAMP的列中,但是这个默认行为可以用@Temporal注解覆盖。
该注解接受来自javax.persistence.TemporalType枚举的单值属性。这提供了三个可能的值:DATE、TIME和TIMESTAMP。这些分别对应于java.sql.Date、java.sql.Time和java.sql.Timestamp。表列在模式生成时被赋予适当的数据类型。下一个清单展示了一个将java.util.Date属性映射为TIME类型的示例——java.sql.Date和java.sql.Time类都是从java.util.Date类派生而来的,因此,令人困惑的是,两者都能够表示数据库中的日期和时间!(java.sql.Date类只公开与日期相关的信息,所以没有时间,而java.sql.Time类只表示时间,没有日期,而Timestamp更类似于旧的java.util.Date。)
@Temporal(TemporalType.TIME)
java.util.Date startingTime;
敏锐的读者还会想到java.time包,它也表示时态数据,尽管比相当粗糙的java.util.Date类要详细得多。java.time类实际上天生就被支持为时态类型,不需要@Temporal注解来指定字段的性质;默认情况下,Hibernate 会适当地映射它们,所以您需要做的就是允许它们被持久化。
其中,LocalDate映射到一个 SQL DATE类型,LocalTime和OffsetTime映射到TIME类型,Instant、LocalDateTime、OffsetDateTime、ZonedDateTime都映射到TIMESTAMP。
如果可以的话,对于日期和时间,使用java.time类而不是java.util类。它们比旧的java.util.Date和java.util.Calendar类定义和规定得更好,因此涉及的关于实际值是什么以及如何将其转换成其他类型的假设也少得多。
元素集合
除了使用一对多映射来映射集合之外,JPA 2 还引入了一个用于映射基本或可嵌入类集合的@ElementCollection注解,比如List或Set。您可以使用@ElementCollection注解来简化您的映射。清单 6-16 展示了一个使用@ElementCollection注解来映射字符串对象集合java.util.List的例子。
package chapter06.embedded;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.List;
@Entity
public class User {
@Id
@GeneratedValue
Long id;
String name;
// this is... not wise from a security perspective
String password;
@ElementCollection
List<String> passwordHints;
}
Listing 6-16src/main/java/chapter06/embedded/User.java
这个类实际上创建了两个表。对于 H2,DDL 看起来是这样的:
create table User (
id bigint not null,
name varchar(255),
password varchar(255),
primary key (id)
);
create table User_passwordHints (
User_id bigint not null,
passwordHints varchar(255)
);
User正在从前面的 DDL 中的一个hibernate_sequence表中获取它的主键。
不要使用 User 这样的类没有太多的谨慎!这里使用password意味着实际密码以明文形式存储在表中。在现实生活中,这基本上是邀请您的用户认证被黑客攻击。你需要使用散列密码,或者编码密码,或者证书,或者……除了明文以外的任何密码。在那个类定义中没有说代码不不散列密码,但是也没有说不散列密码。
您也可以嵌入更复杂的类型,在类型上使用@Embeddable注解。将复杂类型映射为实际实体比映射为嵌入类型可能更有用,但这是可行的。(如果该类型足够复杂,可以被专门查询,那么它可能希望是一个实体,而如果您只关心拥有实体的上下文中的数据,那么嵌入类型可能是合适的。)下面是一个EBook(用于“嵌入的书”,以防止在本章中重复使用“书”40 次)和一个嵌入的Author类型的例子。
在@ElementCollection注解上有两个属性:targetClass和fetch。targetClass属性告诉 Hibernate 哪个类存储在集合中。如果在集合中使用泛型,就不需要指定 targetClass,因为 Hibernate 会推断出正确的类。17fetch属性取一个枚举成员,FetchType。默认情况下为EAGER,但可以设置为LAZY以允许在访问该值时加载。
package chapter06.embedded;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Set;
@Entity
public class EBook {
@Id
@GeneratedValue
Long id;
String name;
@ElementCollection
Set<Author> authors;
}
Listing 6-17src/main/java/chapter06/embedded/EBook.java
package chapter06.embedded;
import javax.persistence.Embeddable;
import java.time.LocalDate;
@Embeddable
public class Author {
String name;
LocalDate dateOfBirth;
}
Listing 6-18src/main/java/chapter06/embedded/Author.java
生成的 SQL(同样针对 H2)如下所示:
create table EBook (
id bigint not null,
name varchar(255),
primary key (id)
);
create table EBook_authors (
EBook_id bigint not null,
dateOfBirth date,
name varchar(255)
);
注意一个给定的作者是如何在一本给定的书中出现多次的(从数据库的角度来看,对给定的Author的任何属性都没有限制18)——同样,@ElementCollection并不是要取代@OneToMany或其同类,如果数据有任何复杂性,考虑使用关系注解而不是嵌入注解是完全合理的。
大型物体
通过应用@Lob 注解,可以将持久化属性或字段标记为数据库支持的大型对象类型。
该注解不带任何属性,但是要使用的底层大型对象类型将从字段或参数的类型中推断出来。String -基于字符的类型将存储在一个合适的基于字符的类型中。所有其他对象,如byte[],将存储在一个 BLOB 中。在这里,我们看到一个String——某种标题19——映射到一个大对象列类型:
@Lob
String title; // a very, very long title indeed
@Lob注解可与@Basic或@ElementCollection注解结合使用。如何在特定数据库中引用该类型在很大程度上取决于所使用的数据库方言。
映射超类
当层次结构的根本身不是一个持久的实体,但从它派生的各种类是时,继承的一个特例就发生了。这样的类可以是抽象的,也可以是具体的。@MappedSuperclass 注解允许您利用这种情况。
标有@MappedSuperclass的类不是一个实体,也不可查询(它不能被传递给期望在Session或EntityManager对象中有实体的方法)。它不能是关联的目标。
超类的列的映射信息将存储在与派生类的细节相同的表中(这样,注解类似于使用带有SINGLE_TABLE策略的@Inheritance注解)。
在其他方面,超类可以被映射为一个普通的实体,但是映射将只应用于派生类(因为超类本身在数据库中没有相关的表)。当一个派生类需要偏离超类的行为时,可以使用@AttributeOverride注解(就像使用一个可嵌入实体一样)。
例如,如果Book是ComputerBook的超类,但是Book对象本身从未被直接持久化,那么Book可以被标记为@MappedSuperclass,如清单 6-19 所示。
package chapter06.mapped;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Book {
@Id
@GeneratedValue
Integer id;
String name;
public Book() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Listing 6-19src/main/java/chapter06/mapped/Book.java
值得注意的是,超类也可以标记为abstract;它没有有具体。超类还需要为子类指定标识符。
下面是从一个Book(列表 6-20 )派生出来的东西。
package chapter06.mapped;
import javax.persistence.Entity;
@Entity
public class ComputerBook extends Book {
String language;
public ComputerBook() {
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
}
Listing 6-20src/main/java/chapter06/mapped/ComputerBook.java
使用这种结构,可以创建一个表:
create table ComputerBook (
id integer not null,
name varchar(255),
language varchar(255),
primary key (id)
);
从Book派生的ComputerBook实体的字段将被存储在ComputerBook实体类的表中。
映射的超类不会而不是将它们的子类标记为实体。直接从Book派生的类,但是没有映射为实体,比如一个假想的MarketingBook类,是不可持久的。仅在这一方面,映射超类方法的行为不同于传统的使用SINGLE_TABLE策略的@Inheritance方法。
使用@OrderColumn 对集合进行排序
虽然@OrderBy允许在从数据库中检索到数据后对其进行排序,但是 JPA 2 还提供了一个注解,允许在数据库中维护适当的集合类型(例如List)的排序,而不是在检索时对进行排序;它通过维护一个 order 列来表示该订单。这里有一个例子:
@OneToMany
@OrderColumn(
name="employeeNumber"
)
List<Employee> employees;
这里,我们声明一个employeeNumber列将维护一个值,从零开始,随着每个条目被添加到列表中而递增。默认起始值可以被基本属性覆盖。默认情况下,该列可以包含 null(无序)值。可空性可以通过将nullable属性设置为 false 来覆盖。默认情况下,当从注解生成模式时,列被假定为整数类型;然而,这可以通过提供一个指定不同列定义字符串的columnDefinition属性来覆盖。(还有更多@OrderColumn的选项,但是不经常用。)
从语义上来说,在这里使用一个Set没有什么意义;一个Set天生无序。
命名查询(HQL 或 JPQL)
@NamedQuery和@NamedQueries允许一个或多个 Hibernate 查询语言或 Java 持久化查询语言(JPQL)查询与一个实体相关联。所需的属性如下:
-
name是检索查询的名称。 -
query是与名称相关联的 JPQL(或 HQL)查询。
清单 6-21 显示了一个将命名查询与Author实体相关联的例子。该查询将按名称检索Author实体,因此很自然地将其与该实体相关联;然而,并没有实际的需求要求一个命名的查询以这种方式与它所关注的实体相关联。(Hibernate 构建了一个命名查询的列表,它们返回的是,而不是与声明它们的位置相关联的。)
@Entity
@NamedQuery(
name="findAuthorsByName",
query="from Author where name = :author"
)
public class Author {
...
}
Listing 6-21A JPQL Named Query Annotation
还有一个hints属性,带有一个QueryHint注解名称/值对,允许应用缓存模式、超时值和各种其他特定于平台的调整(这也可以用于注解查询生成的 SQL)。
您不需要直接将查询与声明它所针对的实体相关联,但是这样做是正常的。如果一个查询与任何实体声明都没有自然的关联,那么可以在包级别进行@NamedQuery注解。 20
没有放置包级注解的自然位置,所以 Java 注解允许一个名为package-info.java的特定文件来包含它们。 21 清单 6-22 给出了这样一个例子。
@javax.annotations.NamedQuery(
name="findBooksByAuthor",
query="from Book b where b.author.name = :author"
)
package chapter06.annotations;
Listing 6-22A package-info.java File
Hibernate 的Session允许直接访问命名查询,如清单 6-23 所示。
Query query = session.getNamedQuery("findBooksByAuthor", Book.class);
query.setParameter("author", "Dave");
List<Book> booksByDave = query.list();
System.out.println("There is/are " + booksByDave.size()
+ " books by Dave in the catalog");
Listing 6-23Invoking a Named Query via the Session
如果你有多个@NamedQuery注解应用于一个实体,它们可以作为@NamedQueries注解的值的数组提供,不管在哪里声明了和@NamedQueries。
命名本机查询(SQL)
Hibernate 还允许使用数据库的本地查询语言(通常是 SQL 的一种方言)来代替 HQL 或 JPQL。如果您使用特定于数据库的特性,您可能会失去可移植性,但是只要您选择合理的通用 SQL,您应该没问题。@NamedNativeQuery注解的声明方式几乎与@NamedQuery注解完全相同。以下代码块显示了一个简单的命名本机查询声明示例:
@NamedNativeQuery(
name="nativeFindAuthorNames",
query="select name from author"
)
所有查询都以相同的方式使用;唯一的区别是它们是如何被访问的,是通过Session.getNamedQuery()、Session.createQuery()还是Session.createSQLQuery();可以通过Query.list()以List的形式检索结果,或者通过Query.scroll()访问可滚动的结果集,Query.iterate()提供了Iterator(惊喜!),如果Query只返回一个对象,就可以使用Query.uniqueResult()。
多个@NamedNativeQuery注解可以与@NamedNativeQueries注解组合在一起。
配置带注解的类
一旦有了带注解的类,就需要将该类提供给应用的 Hibernate 配置,就像它是一个 XML 映射一样。有了注解,您可以使用通过类路径访问的hibernate.cfg.xml XML 配置文档中的声明性配置,或者以编程方式将带注解的类添加到 Hibernate 的org.hibernate.cfg.AnnotationConfiguration对象中。您的应用可以在同一配置中同时使用带注解的实体和 XML 映射的实体。
为了提供一个声明性的映射,我们使用一个普通的hibernate.cfg.xml XML 配置文件,并使用 mapping 元素将带注解的类添加到映射中(参见清单 6-24 )。请注意,我们已经将带注解的类的名称指定为映射。该文件位于相对于项目根目录的src/main/resources或src/test/resources中;这种情况下是chapter06/src/test/resources/hibernate.cfg.xml。
<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:./db6</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- set up c3p0 for use -->
<property name="c3p0.max_size">10</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create</property>
<mapping class="chapter06.primarykey.after.Book"/>
<mapping class="chapter06.compoundpk.CPKBook"/>
<mapping class="chapter06.compoundpk.EmbeddedPKBook"/>
<mapping class="chapter06.compoundpk.IdClassBook"/>
<mapping class="chapter06.twotables.Customer"/>
<mapping class="chapter06.mapped.ComputerBook"/>
<mapping class="chapter06.naturalid.Employee"/>
<mapping class="chapter06.naturalid.SimpleNaturalIdEmployee"/>
<mapping class="chapter06.embedded.User"/>
<mapping class="chapter06.embedded.EBook"/>
<mapping class="chapter06.embedded.Author"/>
<mapping class="chapter06.single.Book"/>
<mapping class="chapter06.single.ComputerBook"/>
<mapping class="chapter06.joined.Book"/>
<mapping class="chapter06.joined.ComputerBook"/>
<mapping class="chapter06.perclass.Book"/>
<mapping class="chapter06.perclass.ComputerBook"/>
</session-factory>
</hibernate-configuration>
Listing 6-24A Hibernate XML Configuration File
您还可以通过编程方式将带注解的类添加到 Hibernate 配置中。注解工具集附带了一个 org . hibernate . CFG . annotation Configuration 对象,该对象扩展了基本 Hibernate 配置对象以添加映射。AnnotationConfiguration 上用于将带注解的类添加到配置中的方法如下:
addAnnotatedClass(Class persistentClass) throws MappingException
addAnnotatedClasses(List<Class> classes)
addPackage(String packageName) throws MappingException
使用这些方法,您可以添加一个带注解的类、一列带注解的类或者一个完整的带注解的类包(按名称)。与 Hibernate XML 配置文件一样,带注解的实体可以与 XML 映射的实体进行互操作。 22
特定于 Hibernate 的持久化注解
Hibernate 有各种注解,它们扩展了标准的持久化注解。它们可能非常有用,但是你应该记住,它们的使用会限制你的应用进入休眠状态;这不会影响我们到目前为止编写的任何代码,因为大部分代码已经使用了特定于 Hibernate 的类。
提示:可移植性的重要性怎么强调都不为过——大多数定制的应用从未部署到他们最初开发的环境之外的环境中。作为一个成熟的产品,除了基本的 JPA 2 规范之外,Hibernate 还提供了许多特性。您不应该浪费太多时间去尝试实现一个比这些专有特性更好的可移植解决方案,除非您对可移植性有明确的要求。如果您需要这些功能,请使用它们。
@不可变
@org.hibernate.annotations.Immutable注解将实体标记为不可变的。这对于您的实体表示引用数据的情况很有用——比如状态、性别或其他很少变化的数据的列表。
因为像州(或国家)这样的东西往往很少更改,所以通常有人通过 SQL 或管理应用手动更新数据。Hibernate 可以主动缓存这些数据,这一点需要考虑在内;如果引用数据发生变化,您需要确保使用它的应用得到通知或以某种方式重新启动。
注解告诉 Hibernate 的是,对不可变实体的任何更新都不应该传递给数据库。它是一个“安全”的物体;一个人可能不应该经常更新它,即使只是为了避免混乱。
@Immutable可以放在一个收藏里;在这种情况下,对集合的更改(添加或删除)将导致抛出一个HibernateException。
自然身份证
本章的第一部分花了很多页讨论主键,包括生成的值。生成的值被称为“人工主键”,强烈建议将 23 作为给定行的一种简写引用。
然而,除了人工或复合主键之外,还有“自然 ID”的概念,它提供了引用实体的另一种便捷方式。
一个例子可能是美国的社会保险号或税务识别号。一个实体(一个人或一个公司)可能有一个由 Hibernate 生成的人工主键,但是它也可能有一个惟一的税务标识符。这可以用@Column(unique=true, nullable=false, updatable=false)来注解,这将创建一个唯一的、不可变的索引, 24 但是一个自然的 ID 也提供了一个可加载的机制,这在我们以前的代码中还没有见过,加上一个实际的优化。
该会话提供了加载器机制的概念,称为“加载访问”Hibernate 中包含三个加载器:能够按 ID 加载、自然 ID 和简单自然 ID。
按 ID 加载是指给定实例的内部引用。例如,如果一个 ID 为 1 的对象已经被 Hibernate 引用,Hibernate 就不需要去数据库加载该对象了——它可以通过 ID 查找该对象并返回引用。
自然本我是本我的另一种形式;在税务标识符的情况下,系统可以通过实际的对象 ID(在大多数情况下是一个人工键)或税务 ID 号本身来查找它——如果税务 ID 是一个“自然 ID”,那么库就能够在内部查找该对象,而不是为数据库构建一个查询。
正如简单标识符和复合标识符分别由单个字段和多个字段组成一样,自然 ID 也有两种形式,同样由单个字段或多个字段组成。
在简单 ID 的情况下,加载过程提供了一个简单的load()方法,所讨论的 ID 是参数。如果不存在具有该 ID 的实例,load()将返回null。加载器还提供了一个替代方法,一个getReference()方法,如果数据库中没有具有该自然 ID 的对象,该方法将抛出一个异常。
对于自然 id,有两种形式的加载机制;一个使用简单的自然 ID(其中自然 ID 是一个且只有一个字段),另一个使用命名属性作为复合自然 ID 的一部分。
现在让我们看一些实际的代码。首先,让我们创建一个表示清单 6-25 中的雇员的类;我们的员工将有一个名字(每个人都有一个名字),一个由数据库分配的人工 ID(一个员工编号),以及一个自然 ID,代表手动分配的工卡编号。
package chapter06.naturalid;
import org.hibernate.annotations.NaturalId;
import javax.persistence.*;
@Entity
public class SimpleNaturalIdEmployee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@NaturalId
Integer badge;
String name;
@Column(scale=2,precision=5,nullable=false)
double royalty;
public SimpleNaturalIdEmployee() {
}
// extra housekeeping not echoed here
}
Listing 6-25A SimpleNaturalIdEmployee Class
简单的自然 ID 是通过用@NaturalId注解单个字段badge来声明的。这使我们能够使用byNaturalId()来获取具有该字段的实体。
要使用加载器机制,您可以通过使用Session.byId()、Session.byNaturalId()或Session.bySimpleNaturalId()获得一个引用,并传递实体的类型。简单加载器(对于 ID 和简单自然 ID)遵循相同的形式:获取加载器,然后加载或获取引用,使用键值作为参数。让我们看看那会是什么样子。首先,我们创建一个测试基类,让我们能够轻松地创建测试数据。
package chapter06.naturalid;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
public class IdTestBase {
protected SimpleNaturalIdEmployee createSimpleEmployee(
String name, int badge
) {
SimpleNaturalIdEmployee employee = new SimpleNaturalIdEmployee();
employee.setName(name);
employee.setBadge(badge);
employee.setRoyalty(10.2385);
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
session.save(employee);
tx.commit();
}
return employee;
}
protected Employee createEmployee(
String name,
int section,
int department
) {
Employee employee = new Employee();
employee.setName(name);
employee.setDepartment(department);
employee.setSection(section);
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
session.save(employee);
tx.commit();
}
return employee;
}
}
Listing 6-26IdTestBase.java
现在让我们看一个使用bySimpleNaturalId()方法的测试。
package chapter06.naturalid;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
public class IdTestSimple extends IdTestBase {
@Test
public void testSimpleNaturalId() {
Integer id = createSimpleEmployee("Sorhed", 5401).getId();
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
SimpleNaturalIdEmployee employee =
session
.byId(SimpleNaturalIdEmployee.class)
.load(id);
assertNotNull(employee);
SimpleNaturalIdEmployee badgedEmployee =
session
.bySimpleNaturalId(SimpleNaturalIdEmployee.class)
.load(5401);
assertEquals(badgedEmployee, employee);
tx.commit();
}
}
}
Listing 6-27IdTestSimple.java
这个代码创建一个新的雇员,有一个特定的工卡号码(5401)。然后,它使用Session.byId(SimpleNaturalIdEmployee.class)为实体获取一个加载器,并调用load(id),使用由createSimpleEmployee()方法返回的 ID。
然而,这里发生了一件有趣的事情,代码演示了不一定从代码级别显而易见的情况。
当我们运行这个方法时,我们实际上加载了两个引用——否则等价性测试没有任何意义。 25 然而,如果我们查看在Session中执行的实际 SQL,我们会看到只发出了一个调用。
这是因为 Hibernate 会将自然 id 缓存在它在会话中加载的对象中。当我们在 load 访问器中使用 natural ID 时,Hibernate 会在会话缓存中查找并找到那个 natural ID——并且知道这是我们正在请求的引用。它不需要去数据库,因为它已经在内存中了。
这有助于使类更加自文档化,也稍微更有效率;这意味着,如果我们有一个来自真实世界的关于一个人的数据,API 会更有效。我们可以通过使用自然索引的工号而不是依赖于其他索引来找到给定的雇员,即使在数据库级别其他索引确实起了作用。
一个具有复合自然 ID 的实体仅仅有更多用@NaturalId标注的字段。让我们创建一个雇员,其部门是自然 ID, 26 ,如清单 6-28 所示。
package chapter06.naturalid;
import org.hibernate.annotations.NaturalId;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@NaturalId
Integer section;
@NaturalId
Integer department;
String name;
public Employee() {
}
// extra housekeeping not echoed here
}
Listing 6-28An Employee Class with a Compound Natural Id
接下来,让我们看一个演示自然 ID 加载器使用的测试,如清单 6-29 所示。 27
package chapter06.naturalid;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
public class NaturalIdTest extends IdTestBase {
@Test
public void testSimpleNaturalId() {
Integer id = createSimpleEmployee("Sorhed", 5401).getId();
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
SimpleNaturalIdEmployee employee =
session
.byId(SimpleNaturalIdEmployee.class)
.load(id);
assertNotNull(employee);
SimpleNaturalIdEmployee badgedEmployee =
session
.bySimpleNaturalId(SimpleNaturalIdEmployee.class)
.load(5401);
assertEquals(badgedEmployee, employee);
tx.commit();
}
}
@Test
public void testLoadByNaturalId() {
Employee initial = createEmployee("Arrowroot", 11, 291);
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Employee arrowroot = session
.byNaturalId(Employee.class)
.using("section", 11)
.using("department", 291)
.load();
assertNotNull(arrowroot);
assertEquals(initial, arrowroot);
tx.commit();
}
}
@Test
public void testGetByNaturalId() {
Employee initial = createEmployee("Eorwax", 11, 292);
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Employee eorwax = session
.byNaturalId(Employee.class)
.using("section", 11)
.using("department", 292)
.getReference();
System.out.println(initial.equals(eorwax));
assertEquals(initial, eorwax);
tx.commit();
}
}
@Test
public void testLoadById() {
Integer id = createEmployee("Legolam", 10, 289).getId();
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Employee boggit = session.byId(Employee.class).load(id);
assertNotNull(boggit);
/*
load successful, let's delete it for the second half of the test
*/
session.delete(boggit);
tx.commit();
}
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Employee boggit = session.byId(Employee.class).load(id);
assertNull(boggit);
tx.commit();
}
}
@Test
public void testGetById() {
Integer id = createEmployee("Eorache", 10, 290).getId();
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Employee boggit = session.byId(Employee.class)
.getReference(id);
assertNotNull(boggit);
/*
* load successful, let's delete it for the second half of the test
*/
session.delete(boggit);
tx.commit();
}
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
try {
Employee boggit = session.byId(Employee.class)
.getReference(id);
// trigger object initialization - which, with a nonexistent object,
// will blow up.
boggit.getDepartment();
fail("Should have had an exception thrown!");
} catch (ObjectNotFoundException ignored) {
}
tx.commit();
}
}
}
Listing 6-29The Natural Id Loader in Action
在testLoadByNaturalId()中,我们看到了与我们之前的自然 ID 使用测试非常相似的东西:我们创建一个雇员,然后搜索 ID。由Session.byNaturalId()返回的对象有一个using()方法,它接受一个字段名和字段值,而不是使用一个对标识符的引用。如果我们不包括组成自然 ID 的每个字段,我们将得到一个异常。
注意,我们使用的是load()方法;如果数据库中不存在自然 ID,load()将返回一个 null 信号值。
NaturalIdTest中的另一种测试方法是testGetByNaturalId()。如果 id 值不存在,这个使用getReference()的函数将抛出一个异常,所以我们不需要检查null。
摘要
在这一章中,我们使用 JPA 2 注解来为 Hibernate 的 POJOs 添加元数据,并且我们看到了一些特定于 Hibernate 的注解,这些注解可以以降低可移植性为代价来增强这些功能。
在下一章,我们将讨论 Hibernate 的 JPA 配置,更多的对象生命周期和数据验证。
七、JPA 集成和生命周期事件
Hibernate 提供了许多简单的“原生 Hibernate API”之外的功能在这一章中,我们将讨论如何使用标准的 JPA 配置资源、Hibernate 的对象验证工具、对象生命周期事件以及一些其他技巧。
Java 持久化 API
Java Persistence API,简称 JPA,是由 Java 社区过程批准的标准,来自许多项目和供应商的代表的输入——并且受到 Hibernate 的很大影响。它是作为新的企业 Java 规范的一部分创建的,主要是因为实体 Beans 企业持久化的先前标准——很难编写和使用,更难很好地使用。 1
Hibernate 参与了创建 JPA 的社区团队,公平地说,JPA 规范与 Hibernate 的 API 非常相似;Hibernate 本身集成了许多 JPA 实践,正如前面关于映射的章节所示。(大多数映射特性和注释都是 JPA 规范的一部分;现在,这些注释的原生 Hibernate 版本已经可用,但很少在实践中使用。 2
Hibernate 提供了 JPA 规范的实现。因此,您可以直接使用 JPA,带有 JPA 特定的配置文件,并获取一个EntityManager而不是一个Session。
您可能想这样做有几个原因。首先,JPA 是一个标准,这意味着符合标准的代码通常是可移植的,允许不同实现之间的差异。例如,您可以将 Hibernate 用于开发,而对于生产,您可以部署到提供 EclipseLink 的应用服务器中。(反之亦然:您可以使用 EclipseLink 进行开发,并部署到使用 Hibernate 的架构中。)
另一个原因是 Java EE 规范本身。Java EE 容器需要提供 JPA,这意味着容器可以管理和配置资源;利用非 JPA 配置会给应用开发人员带来更大的负担。然而,值得指出的是,即使在默认为不同 JPA 实现的容器中,也可以使用 Hibernate 作为 JPA 实现,这为您提供了两全其美的好处:JPA 配置标准(在某些方面有其自身的好处)和 Hibernate 的出色性能和扩展特性集。 3
因此,让我们看看我们需要做些什么来支持 JPA 配置文件,而不是 Hibernate 配置过程。我们将通过一系列简单的步骤为我们提供一个工作工具包。它们是:
-
将 Hibernate 的 JPA 支持添加到
util项目中,作为一个非转换依赖项。4 -
添加一个
JPASessionUtil类,作为SessionUtil实用程序的近似模拟。就像SessionUtil提供了一个Session实例一样,JPASessionUtil将提供一个EntityManager实例,我们还将添加一个机制,通过这个机制它将提供一个 HibernateSession;这样,我们可以通过 Hibernate API 使用 JPA 配置。 -
编写 JPA 配置和测试来展示功能操作;这将让我们了解 JPA 和 Hibernate 之间的一些差异。
项目对象模型
再来看看util项目的pom.xml。我们在第三章中提到了它,但是跳过了它的细节,因为这本书的源代码有完整的形式(这里有一个秘密——我们知道我们会在这一章回顾它)。)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hibernate-6-parent</artifactId>
<groupId>com.autumncode.books.hibernate</groupId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>util</artifactId>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-hikaricp</artifactId>
<version>${hibernate.core.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Listing 7-1util/pom.xml
这大部分都很普通,但是请注意包含了lombok和 at test范围。这种依赖用于自动生成 Java 类的样板文件——我们将在本章的下一节深入讨论它。我们将它定义为test,因为我们希望能够编写使用它的代码(并测试它),但是我们不希望而不是强制任何使用它的项目将lombok作为显式依赖;我们基本上将其标记为非转换依赖。
毕竟,可传递的依赖关系是从一个项目传递到另一个项目的;如果我们说项目 A 依赖于依赖项 B,那么任何使用 A 的项目都必须也依赖于 B。需要显式地包含非传递依赖项,因此如果 A 对工件 D 有非传递依赖项——比如lombok,如此处所示——使用 A 的项目必须显式地声明对 D 的依赖项
现在让我们先简单地讨论一下样板文件,因为现在是时候用 Lombok 去掉很多样板文件了。
介绍龙目岛
“样板文件”,在我们的上下文中,是一些被一遍又一遍重复使用而没有重大改变的东西。我们总是在简单的访问器和变异器中看到它;当我们有一个String name的时候,我们期望有两个方法伴随着它:
String getName() { return name; }
void setName(String name) { this.name=name; }
我们在equals()和hashCode()身上看到了同样的事情,就此而言,toString()也是如此。当然,这些方法在不同的领域和不同的类中是不同的,但是它们本质上都是相同的,只是在细节上有所不同——通常 IDE 可以为我们生成它们。
这样做的问题是,它为这些方法创造了一种“常态”。只要他们总是做完全相同的事情,那可能没问题;在本书中,我们倾向于不在源代码中打印这些方法,因为它们非常重复,并且没有提供读者会发现有用的信息。
危险在于当实现和与标准不同时;例如,如果我们的访问器(getName())要返回一个name字段的规范化版本,我们作为读者和程序员已经习惯了样板,一个与其他样板方法做的事情不完全相同的方法根本不会引人注目。
因此,尽管样板代码并不坏,但如果我们不必包含它,那会更好。
许多语言,包括更高版本的 Java(即 15 或更高版本),都提供了为类提供这种样板文件的语法。Java 14 引入了record,它是一个不可变的类,为您提供了访问器和其他标准方法。但是,记录不适合用 Hibernate 或 JPA 持久化,因为需要代理一个类来更新数据;记录不能被框架改变(因为它们是不可变的),因此不能被延迟初始化,这是一个对性能非常重要的特性。
Java 中的记录最适合数据传输,而不是持久化。
然而,Lombok ( https://projectlombok.org )允许我们简单而干净地注释一个对象,这样我们就可以显示与该实体相关的所有代码,甚至可以从源代码中删除所有的样板代码。
Lombok 提供了许多注释,可以生成我们刚刚提到的所有样板方法,以及更多:toString()、equals()、hashCode()、赋值函数、访问函数和无参数构造函数,等等。Lombok 是编译时依赖项;我们不需要库存在于任何依赖于生成的类的东西中。
我们已经将它包含在我们的pom.xml中,但是这里是具体的依赖关系;它作为一个非传递依赖项包含在这里。
我们希望 Lombok 是一个非传递依赖。它是一个注释处理器,只在编译时运行;在它生成的内容中没有下游依赖项,因此从部署的角度来看,可传递依赖项没有任何意义。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>test</scope>
</dependency>
那么龙目岛为我们做了什么?它实际上根据我们想要它做的事情生成样板代码。例如,我们可以用@Getter来注释一个字段,它将基于该字段的名称生成一个适当的 JavaBean 兼容的访问器方法。甚至还有一种包罗万象的注释,@Data,它将为所有字段生成访问器和变异器,以及对equals()、hashCode()和toString()的良好实现,这样我们就可以拥有一个完整的和完整的实体,其清单如清单 7-2 所示。
package com.autumncode.util.model;
import lombok.Data;
import javax.persistence.*;
@Entity(name = "Thing")
@Data
public class Thing {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@Column
String name;
}
Listing 7-2util/src/test/java/com/autumncode/util/model/Thing.java
在通过 Lombok 注释处理器进行编译和处理之后,这个类将拥有setId(Integer)、getId()、setName(String)、getName()、一个与id和name相比较的equals()实现,以及一个兼容的hashCode()实现,还有一个包含所有属性的toString()。该源代码就是我们所需要的全部——不再像我们在前面的清单中看到的那样“为了简洁而删除代码”。
我们在 Lombok 中也有选项来指定不同种类的构造函数;我们的Thing暂时保留了默认的构造函数。
在我们探索本章的其余部分时,我们将使用这个类。
JPASessionUtil 类
JPA 使用“持久化单元”的概念,这些单元被命名为配置。在给定的部署中,每个持久化配置都有一个唯一的名称。因为持久化单元是命名的,所以我们需要考虑我们的实用程序类有多个持久化单元的可能性,如清单 7-3 所示。
package com.autumncode.jpa.util;
import org.hibernate.Session;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.HashMap;
import java.util.Map;
public class JPASessionUtil {
private static Map<String, EntityManagerFactory>
persistenceUnits = new HashMap<>();
@SuppressWarnings("WeakerAccess")
public static synchronized EntityManager
getEntityManager(String persistenceUnitName) {
persistenceUnits
.putIfAbsent(
persistenceUnitName,
Persistence
.createEntityManagerFactory(
persistenceUnitName
));
return persistenceUnits
.get(persistenceUnitName)
.createEntityManager();
}
public static Session getSession(String persistenceUnitName) {
return getEntityManager(persistenceUnitName)
.unwrap(Session.class);
}
}
Listing 7-3util/src/main/java/com/autumncode/jpa/util/JPASessionUtil.java
原谅格式;这些都是很长的方法调用,而且它们都是链式的,所以尽管在进行链式调用时没有额外的复杂性,但从概念上讲,它们最终看起来比实际长得多。
在这个类中,我们建立了一种重用EntityManagerFactory实例的方法,通过名字来查找。如果给定的名称没有EntityManagerFactory,我们将创建并保存它。如果给定名称不存在持久化单元,将抛出一个javax.persistence.PersistenceException。
如果您使用的是一个为您管理持久化单元的框架,比如 Jakarta EE 或 Spring,那么这段代码就没有用了。事实上,几乎任何提供 JPA 集成的框架都会使这个类变得完全没有必要,这也是它不是很长的部分原因。我们使用它主要是为了让后续章节中编写示例代码更加方便。
getSession()方法提供了对EntityManager底层实现的访问。对于 Hibernate,这将是org.hibernate.Session;如果实际的实现不是 Hibernate,那么就会抛出一个运行时异常。
所有这些都是有用的,但是让我们开始使用它。让我们写一些测试来展示如何使用这个类。
测试 JPASessionUtil
我们的第一个测试只是试图获取资源:一组正确配置的资源和另一组没有正确配置的资源。这将允许我们验证该实用程序是否返回了它所期望的结果,即使是在配置不当的情况下。清单 7-4 显示了我们第一套测试的代码;接下来,我们将使用这些测试将使用的 JPA 配置。
package com.autumncode.jpa.util;
import com.autumncode.util.model.Thing;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import static org.testng.Assert.*;
public class JPASessionUtilTest {
@Test
public void getEntityManager() {
EntityManager em = JPASessionUtil
.getEntityManager("utiljpa");
em.close();
}
@Test(
expectedExceptions = {javax.persistence.PersistenceException.class}
)
public void nonexistentEntityManagerName() {
JPASessionUtil.getEntityManager("nonexistent");
fail("We shouldn't be able to acquire an EntityManager here");
}
@Test
public void getSession() {
Session session = JPASessionUtil.getSession("utiljpa");
session.close();
}
@Test(
expectedExceptions = {javax.persistence.PersistenceException.class}
)
public void nonexistentSessionName() {
JPASessionUtil.getSession("nonexistent");
fail("We shouldn't be able to acquire a Session here");
}
@Test
public void testEntityManager() {
EntityManager em = JPASessionUtil.getEntityManager("utiljpa");
em.getTransaction().begin();
Thing t = new Thing();
t.setName("Thing 1");
em.persist(t);
em.getTransaction().commit();
em.close();
em = JPASessionUtil.getEntityManager("utiljpa");
em.getTransaction().begin();
TypedQuery<Thing> q = em.createQuery(
"from Thing t where t.name=:name",
Thing.class);
q.setParameter("name", "Thing 1");
Thing result = q.getSingleResult();
assertNotNull(result);
assertEquals(result, t);
em.remove(result);
em.getTransaction().commit();
em.close();
}
@Test
public void testSession() {
Thing t = null;
try (Session session = JPASessionUtil.getSession("utiljpa")) {
Transaction tx = session.beginTransaction();
t = new Thing();
t.setName("Thing 2");
session.persist(t);
tx.commit();
}
try (Session session = JPASessionUtil.getSession("utiljpa")) {
Transaction tx = session.beginTransaction();
Query<Thing> q =
session.createQuery(
"from Thing t where t.name=:name",
Thing.class);
q.setParameter("name", "Thing 2");
Thing result = q.uniqueResult();
assertNotNull(result);
assertEquals(result, t);
session.delete(result);
tx.commit();
}
}
}
Listing 7-4util/src/test/java/com/autumncode/jpa/util/JPASessionUtilTest.java
您会注意到不存在的测试做了一些奇怪的事情:它们声明预期的Exception类型。通常,异常意味着测试失败;在这种情况下,我们说如果抛出一个匹配的异常,测试没有失败。
然而,“没有失败”并不等同于“通过”对于这些测试,我们实际上希望失败,除非遇到异常;因此,我们试图获取资源并调用fail()——在fail()执行之前,一个异常将退出该方法,这意味着测试通过。
然而,除非我们包含一个 JPA 配置文件,否则这些测试都不会通过,这个文件需要在类路径的/META-INF/persistence.xml处,如清单 7-5 所示。
<persistence
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="utiljpa">
<properties>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:./utiljpa"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
Listing 7-5util/src/test/resources/META-INF/persistence.xml
创建了这个文件后,我们就有了一个有效的持久化单元,名为utiljpa;我们现在可以运行四个测试了。通过它们的传递,您可以看到(并证明)当被请求时,JPASessionUtil返回一个EntityManager和Session的实例,并在发出无效请求时抛出一个异常。
正如你在testEntityManager()方法中看到的,我们使用了一个Thing——我们的 Lombok 注释的实体类——就像它是一个常规的 POJO 一样,显式地调用setName()(并通过assertEquals()隐式地调用equals())。
您还会注意到testSession()方法。这和testEntityManager()是功能等同的测试。
在每一个中,我们有两个操作。在每一个中,我们获得一个提供持久化的类, 6 启动一个事务,持久化一个Thing实体,然后提交事务;然后我们重复这个过程,查询然后删除实体。这两种方法之间的唯一区别是使用的持久化 API 测试使用 JPA,testSession()使用 Hibernate API。
大多数区别都相当简单:例如,JPA 使用EntityManager.remove()而不是Session.delete()。查询类型是不同的(JPA 的类型化查询是一个javax.persistence.TypedQuery,而 Hibernate 的是一个org.hibernate.query.Query),尽管它们在功能上是相同的。可能最相关的变化是事务的使用,这完全是自愿的。例如,您可以在testSession()方法中使用清单 7-6 中所示的块,这使得它几乎与 JPA 版本完全相同。
try(Session session = JPASessionUtil.getSession("utiljpa")) {
session.getTransaction().begin();
Thing t = new Thing();
t.setName("Thing 2");
session.persist(t);
session.getTransaction().commit();
}
Listing 7-6Mirroring the EntityManager API with Session
然而,重要的是要注意到Session和EntityManager是相似的,但不是相同的;虽然清单 7-6 如果你使用EntityManager而不是Session会工作,即使在测试代码的小块中Session使用org.hibernate.query.Query而不是javax.persistence.TypedQuery。
那么应该用哪一个呢?嗯,这取决于你需要什么。如果您需要 JPA 兼容性,那么您必须限制自己使用EntityManager及其功能;否则,请使用您喜欢的方法。Hibernate API 提供了一些 JPA 无法提供的微调特性;如果你想使用它们,你会想使用Session,但是除此之外,这两个 API 在大多数意图和目的上是相同的。
生命周期事件
Java 持久化 API 向数据模型公开某些事件。这些事件允许开发人员实现架构本身不容易提供的附加功能。事件通过使用注释来指定,事件处理程序可以直接嵌入到实体中,也可以保存在单独的实体侦听器类中。
您可以以几种不同的方式使用生命周期:例如,您可以手动更新时间戳,或者您可以编写审计数据,初始化瞬态数据,或者在持久化数据之前验证数据。
存在与对象创建、读取、更新和删除相对应的生命周期事件。对于在持久化上下文中有意义的每种事件类型,在事件发生之前和之后都有回调挂钩。
事件处理程序是对应于七个生命周期阶段之一的简单方法。
表 7-1
实体生命周期阶段
|生命周期注释
|
方法运行时
|
| --- | --- |
| @PrePersist | 在数据实际插入数据库表之前执行。当数据库中存在对象并且发生更新时,不使用它。 |
| @PostPersist | 在数据写入数据库表后执行。 |
| @PreUpdate | 更新托管对象时执行。当对象第一次保存到数据库中时,不使用此注释。 |
| @PostUpdate | 在将托管对象的更新写入数据库后执行。 |
| @PreRemove | 在从数据库中删除托管对象的数据之前执行。 |
| @PostRemove | 从数据库中删除托管对象的数据后执行。 |
| @PostLoad | 在从数据库加载托管对象的数据并初始化该对象后执行。 |
清单 7-7 提供了一个实体,描述性地命名为“LifecycleThing”,它为各种生命周期事件提供挂钩。与我们之前的类一样,它使用 Lombok 来隐藏样板文件,这样这就是实际的完整源代码清单。 7
package chapter07.lifecycle;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.*;
import java.util.BitSet;
@Entity
@Data
public class LifecycleThing {
static Logger logger = LoggerFactory.getLogger(LifecycleThing.class);
static BitSet lifecycleCalls = new BitSet();
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@Column
String name;
@PostLoad
public void postLoad() {
log("postLoad", 0);
}
@PrePersist
public void prePersist() {
log("prePersist", 1);
}
@PostPersist
public void postPersist() {
log("postPersist", 2);
}
@PreUpdate
public void preUpdate() {
log("preUpdate", 3);
}
@PostUpdate
public void postUpdate() {
log("postUpdate", 4);
}
@PreRemove
public void preRemove() {
log("preRemove", 5);
}
@PostRemove
public void postRemove() {
log("postRemove", 6);
}
private void log(String method, int index) {
lifecycleCalls.set(index, true);
logger.info("{}: {} {}", method,
this.getClass().getSimpleName(), this);
}
}
Listing 7-7src/main/java/chapter07/lifecycle/LifeCycleThing.java
这个类跟踪在一个BitSet中进行的生命周期调用。当生命周期事件发生时,它在BitSet中设置一个位;一个测试可以(并且将会)检查BitSet以确保没有缺口,这将会给我们一个更清晰的画面,我们是否已经成功地执行了每个回调。
当然,我们可以只用我们的眼睛来观察结果。这当然是可行的(可悲的是,这是大多数用户测试的基础),但是我们想要客观的、可重复的、更可验证的结果。
我们的生命周期测试如清单 7-8 所示。它需要做的只是创建、读取、更新和删除一个实体;这将触发我们的每个事件处理程序,我们可以看到序列(如果我们观察应用日志)并让测试验证没有跳过任何测试(因为它检查位集)。
package chapter07.lifecycle;
import com.autumncode.jpa.util.JPASessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.Reporter;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
public class FirstLifecycleTest {
@Test
public void testLifecycle() {
Integer id;
LifecycleThing thing1, thing2, thing3;
try (Session session = JPASessionUtil.getSession("chapter07")) {
Transaction tx = session.beginTransaction();
thing1 = new LifecycleThing();
thing1.setName("Thing 1");
session.save(thing1);
id = thing1.getId();
System.out.println(thing1);
tx.commit();
}
try (Session session = JPASessionUtil.getSession("chapter07")) {
Transaction tx = session.beginTransaction();
thing2 = session
.byId(LifecycleThing.class)
.load(-1);
assertNull(thing2);
Reporter.log("attempted to load nonexistent reference");
thing2 = session.byId(LifecycleThing.class)
.getReference(id);
assertNotNull(thing2);
assertEquals(thing1, thing2);
thing2.setName("Thing 2");
tx.commit();
}
try (Session session = JPASessionUtil.getSession("chapter07")) {
Transaction tx = session.beginTransaction();
thing3 = session
.byId(LifecycleThing.class)
.getReference(id);
assertNotNull(thing3);
assertEquals(thing2, thing3);
session.delete(thing3);
tx.commit();
}
assertEquals(LifecycleThing.lifecycleCalls.nextClearBit(0), 7);
}
}
Listing 7-8src/test/java/chapter07/lifecycle/FirstLifecycleTest.java
这个测试有三个部分,每个部分都使用自己的会话和事务。第一个创建一个LifecycleThing并持久化它。第二次尝试加载一个不存在的实体,然后加载一个现有的实体;然后,它更新现有的实体。第三部分加载相同的实体并删除它。这意味着我们表示了对象中的每个生命周期事件:创建、读取、更新和删除。
对于每个生命周期事件,都会生成一条日志消息。同时,修改内部BitSet来跟踪生命周期方法是否已经被调用;在测试结束时,检查BitSet以查看从 7 开始的每个位都已被设置。如果值是正确的,那么我们知道每个生命周期方法至少被调用过一次。
结果应该相当明显:在这种情况下,在持久化发生之前调用prePersist(),在持久化发生之后运行postPersist()。生命周期处理程序中的异常可能很棘手。如果在事件之前生命周期监听器中发生异常——也就是说,在@PrePersist、@PreUpdate或@PreRemove中——它将被传递给调用者进行处理。然而,该事务仍然有效。也就是说,如果@PostPersist、@PostUpdate、@PostRemove或@PostLoad代码出现错误,您将使事务无效。
后加载操作中的异常处理起来会很有趣。(从对象的角度来看,这表明数据库中的数据是无效的;例如,考虑数据库中的字段是否具有来自枚举的值范围,并且这是在加载操作之后以编程方式检查的。)它可能必须在数据库本身中处理,建议您不惜一切代价避免这种可能性。
外部实体侦听器
LifecycleThing最大的弱点(除了它是一个唯一目的是说明持久化生命周期的类之外)是所有的事件监听器都嵌入在类本身中。相反,我们可以通过使用@EntityListeners注释,将一个外部类指定为具有相同注释的实体监听器。清单 7-9 显示了一个带有外部实体监听器的简单实体。
package chapter07.lifecycle;
import lombok.*;
import javax.persistence.*;
@Entity
@NoArgsConstructor
@Data
@EntityListeners({UserAccountListener.class})
public class UserAccount {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
String name;
@Transient
String password;
Integer salt;
Integer passwordHash;
public boolean validPassword(String newPass) {
return newPass.hashCode() * salt == getPasswordHash();
}
}
Listing 7-9src/main/java/chapter07/lifecycle/UserAccount.java
清单 7-10 展示了一个简单的外部监听器可能是什么样子。
package chapter07.lifecycle;
import javax.persistence.PrePersist;
public class UserAccountListener {
@PrePersist
void setPasswordHash(Object o) {
UserAccount ua = (UserAccount) o;
if (ua.getSalt() == null || ua.getSalt() == 0) {
ua.setSalt((int) (Math.random() * 65535));
}
ua.setPasswordHash(
ua.getPassword().hashCode() * ua.getSalt()
);
}
}
Listing 7-10src/main/java/chapter07/lifecycle/UserAccountListener.java
当UserAccount被持久化时,UserAccountListener将设置一个哈希密码,乘以一个随机盐;据推测,用户提供的密码可以通过应用相同的 salt 来测试。 8 (这是不安全的,无论如何。不要用这段代码作为安全性的例子。)
在这种情况下,侦听器只监控一种对象类型;它不进行错误检查。(如果传递给它的类型不正确,它将抛出一个错误。)
事件监听器可以方便地将您实际需要访问持久化生命周期的任何地方考虑在内,尤其是在考虑数据验证时。
数据有效性
从现在开始,我们将开始看到不是“当前版本”的库,这取决于它们是否已经更新到使用 Jakarta EE 打包。Hibernate 6 仍然在使用 JPA 的javax.persistence打包,而不是jakarta.persistence,并且像 Validator API 这样的东西的版本被选择为尽可能符合旧的javax前缀。Hibernate 有一个jakarta.persistence迁移,但是它还不是“正常”的方法,在它之前,那些迁移应该被认为是“测试中”而不是“生产就绪”,即使它们可能很好。与此同时,同时使用jakarta和javax会令人困惑,所以在迁移到jakarta完成之前,我们将继续使用javax前缀。
Hibernate 还提供了一个验证 API,目前是 Java 的 Bean 验证规范 3.0 版的参考实现。9Bean 验证规范允许您的数据模型强制执行自己的约束,而不是让编码人员在整个应用代码中添加自己的数据值检查。
基于模型的验证应该有明显的价值:这意味着无论在哪个阶段访问数据,您都能够信任模型的状态。
考虑在 web 服务中应用数据验证的情况;离开 web 服务访问数据可能不会应用验证,这意味着与从其他环境访问数据相比,您更信任通过 web 服务访问的数据。这是一件坏事。
注意,作为 JPA 规范本身的一部分,我们已经有了一些验证功能。例如,我们可以指定列的值是惟一的(通过@Id或@Column(unique=true);我们还可以通过@Column(nullable=false)指定列不能为空。通过实体生命周期的魔力,我们还可以通过回调和外部监听器来实施数据验证, 10 值得注意的是,在某些情况下,这仍然是一种有价值的、可行的方法。
因此,让我们来看看我们可以做些什么来尝试 Hibernate 的一些更强大的验证功能。
第一步是将 Hibernate 验证器添加到我们的项目中。如果您在 Java SE 项目中使用 Validator(一个独立的应用,比如我们的测试),那么您需要添加四个依赖项;如果像 WildFly 一样将应用部署到 Java EE 应用服务器中,只需要添加验证器依赖项本身。
清单 7-11 显示了本章的完整pom.xml。请注意,它使用占位符来表示其依赖项的版本;这本书的源代码被组织成一个单独的项目,这些版本被指定为顶层项目中的属性。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hibernate-6-parent</artifactId>
<groupId>com.autumncode.books.hibernate</groupId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter07</artifactId>
<dependencies>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>util</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate.validator.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-cdi</artifactId>
<version>${hibernate.validator.version}</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>${javax.el-api.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>${javax.el-api.version}</version>
</dependency>
</dependencies>
</project>
Listing 7-11chapter07/pom.xml
现在让我们看一个使用验证来确保数据正确性的类和测试。首先是ValidatedPerson类,如清单 7-12 所示。
package chapter07.validated;
import lombok.*;
import javax.persistence.*;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Entity
@Data
@Builder
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@NoArgsConstructor
public class ValidatedPerson {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column
@NotNull
@Size(min = 2, max = 60)
String fname;
@Column
@NotNull
@Size(min = 2, max = 60)
String lname;
@Column
@Min(value = 13)
Integer age;
}
Listing 7-12src/main/java/chapter07/validated/ValidatedPerson.java
我们实际上已经通过 Lombok 给这个实体添加了一些东西。我们首先要研究的是@AllArgsConstructor注释,它创建了一个包可见的构造函数,所有属性都作为参数;就好像我们创造了ValidatedPerson(Long id, String fname, String lname, Integer age)。我们将它设置为 package-visible,因为我们不希望任何其他类使用它,主要是因为我们使用了另一个 Lombok 注释,@Builder。
@Builder注释创建了一个内部类,可以通过builder()方法访问。 11 这个内部类使用了一个流畅的 API 12 提供了一种便捷的构造类的方式;有了生成器,我们可以使用下面的代码来构造一个ValidatedPerson:
ValidatedPerson person=ValidatedPerson.builder()
.age(15)
.fname("Johnny")
.lname("McYoungster")
.build();
现在让我们看看我们正在使用的验证注释,以及为什么。值得注意的是,我们并没有使用 Validator 提供给我们的所有注释——目前有超过 25 个注释被记录在案,这还不包括定制验证器的可能性。这些只是一些常用的验证注释。
第一个突出的是@NotNull,用在fname属性上。这类似于我们之前提到的@Column(nullable=false)注释,但是应用于持久化生命周期的不同点;如果使用了@NotNull,列仍然会以同样的方式设置(不允许空值),但是验证发生在持久化之前。如果我们使用@Column(nullable=false),验证发生在数据库中,并给我们一个数据库约束违反,而不是验证失败——这是一个非常微小的语义差异,但仍然是一个差异。
使用@Column(length=60)可以部分模拟@Size,但是@Column没有办法强制最小大小约束,并且验证阶段发生在持久化阶段之前。
@Min(value=13)指定整数值有一个最小值,正如人们可能预料的那样;最大值有相应的@Max注释。
其中一件有趣的事情是,它们实际上可以改变数据库定义。例如, 13 @Min和@Max在数据库支持的情况下添加表约束,@NotNull在代码和数据库级别强制执行约束。@Size如果给定了最大大小,将为数据库列指定一个最大大小;数据库通常不会强制要求最小大小。
让我们在测试中看看这是什么样的。我们要做的是将一系列对象写入 Hibernate Session中,其中大部分将以某种方式通过验证。实际的持久化机制听起来像是我们可以为其编写一个方法的东西,所以不再多说, 14 让我们看看清单 7-13 中的整套测试,这样我们就可以看到验证是如何应用的。
package chapter07.validator;
import chapter07.unvalidated.UnvalidatedSimplePerson;
import chapter07.validated.ValidatedPerson;
import com.autumncode.hibernate.util.SessionUtil;
import javax.validation.ConstraintViolationException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;
import static org.testng.Assert.fail;
public class ValidatorTest {
private ValidatedPerson persist(ValidatedPerson person) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
session.persist(person);
tx.commit();
}
return person;
}
@Test
public void createUnvalidatedUnderagePerson() {
Long id = null;
try (Session session = SessionUtil.getSession()) {
Transaction transaction = session.beginTransaction();
UnvalidatedSimplePerson person = new UnvalidatedSimplePerson();
person.setAge(12); // underage for system
person.setFname("Johnny");
person.setLname("McYoungster");
// this succeeds because the UnvalidatedSimplePerson
// has no validation in place.
session.persist(person);
id = person.getId();
transaction.commit();
}
}
@Test
public void createValidPerson() {
persist(ValidatedPerson.builder()
.age(15)
.fname("Johnny")
.lname("McYoungster").build());
}
@Test(expectedExceptions = ConstraintViolationException.class)
public void createValidatedUnderagePerson() {
persist(ValidatedPerson.builder()
.age(12)
.fname("Johnny")
.lname("McYoungster").build());
fail("Should have failed validation");
}
@Test(expectedExceptions = ConstraintViolationException.class)
public void createValidatedPoorFNamePerson2() {
persist(ValidatedPerson.builder()
.age(14)
.fname("J")
.lname("McYoungster2").build());
fail("Should have failed validation");
}
@Test(expectedExceptions = ConstraintViolationException.class)
public void createValidatedNoFNamePerson() {
persist(ValidatedPerson.builder()
.age(14)
.lname("McYoungster2").build());
fail("Should have failed validation");
}
}
Listing 7-13src/test/java/chapter07/validator/ValidatorTest.java
清单中的第一个方法——persist()—练习持久化循环,以节省代码。我们的测试方法将创建一个对象,并将其传递给 this 来执行验证生命周期。
我们的其他四个方法创建匹配各种单一标准的实体:一个有效的实体,一个fname太短的实体,一个lname太短的实体,一个没有fname的实体,以及一个underage的实体。在我们预期验证失败的测试中,我们将方法标记为接受异常——如果persist()方法成功执行,则方法失败。这对我们有用,因为我们预计persist()方法在这些情况下会失败。
值得注意的一件事是所有这些代码是如何重复的:我们有很多改变字段值的测试,因此我们的测试看起来都是一样的。我们可以做得比这更好——通常使用的测试框架实际上提供了这一点。我们可以参数化我们的测试,在这里我们声明一个生成输入的数据提供者方法,测试框架将调用我们所有数据集的参数化测试方法,将它们视为单独的测试。
清单 7-14 显示了ValidatorTest的参数化版本。
package chapter07.validator;
import chapter07.unvalidated.UnvalidatedSimplePerson;
import chapter07.validated.ValidatedPerson;
import com.autumncode.hibernate.util.SessionUtil;
import lombok.val;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.IExpectedExceptionsHolder;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import javax.validation.ConstraintViolationException;
import static org.testng.Assert.fail;
public class ParameterizedTest {
private ValidatedPerson persist(ValidatedPerson person) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
session.persist(person);
tx.commit();
}
return person;
}
@DataProvider
Object[][] provider() {
return new Object[][]{
{"Johnny", "McYoungster", 15, false},
{"Johnny", "McYoungster", 12, true},
{"J", "McYoungster", 14, true},
{"Johnny", "M", 14, true},
{"Johnny", null, 14, true},
};
}
@Test(dataProvider = "provider")
void testValidations(String fname, String lname, Integer age, boolean expectException) {
try {
val builder=ValidatedPerson
.builder()
.age(age)
.fname(fname);
if(lname!=null) {
builder.lname(lname);
}
persist(builder.build());
if (expectException) {
fail("should have caught an exception");
}
} catch (Exception ex) {
if (!expectException) {
fail("expected an exception");
}
}
}
}
Listing 7-14src/test/java/chapter07/validator/ParameterizedTest.java
我们在这里所做的是声明一个方法,该方法返回一个对象数组的数组——provider()——并给它四个值:名、姓、年龄和一个布尔值,该值指示数据集是否“有效”。这些值将在我们的测试方法testValidations()中进行位置设置。
测试方法本身比ValidatorTest中的版本稍微复杂一些,因为我们希望能够测试缺失的值。我们实际上并没有像这里的和一样完整——我们只有一个缺失的lname的检查——但是这个概念适用于每个参数。我们还使用了 Java 11 的val关键字,因为我们可以让 Java 推断出builder的类型——它实际上是ValidatedPerson.ValidatedPersonBuilder。
在try/catch结构的每个分支中,我们使用expectException值来确定结果是否是我们想要的;如果我们到达了try的末尾,没有出现我们期望的异常,那么我们就没有通过测试,并且我们反转了catch子句的机制。如果您想要验证这是否如预期的那样工作,更改provider()方法中的值,这也集中了所有测试数据的去向。 15
不过,您可能会注意到,我们的验证只包含单个属性。我们可以使用实体生命周期来创建我们自己的定制验证,但是 Validator 允许我们创建我们自己的验证注释——包括单字段验证(正如我们已经看到的)和类级验证。
让我们创建一个坐标实体——为了举例,让我们使用一个验证来确保一个有效的坐标不允许出现在笛卡尔象限系统的象限 III 中。(象限 III 中的坐标具有负的 x 和 y 属性。)单字段验证在这里不起作用,因为–5 作为 x 坐标是有效的,只要 y 坐标也不是负的。
我们实际上有很多选项可以选择来构建验证。最灵活的选项是一个查找依赖字段的注释——因此,对 X 的验证将包含对 Y 的引用以及附带的可接受标准,反之亦然。也就是说,让我们选择一个更简单的选项,一个特定于我们的坐标类的选项。 16
首先,我们来看看Coordinate类。然后我们将创建我们期望通过的测试;最后,我们将看看应用验证的注释。与SimpleValidatedPerson实体非常相似,我们将大量使用 Lombok 来消除样板代码。
package chapter07.validated;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@NoQuadrantIII
public class Coordinate {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@NotNull
Integer x;
@NotNull
Integer y;
}
Listing 7-15src/main/java/chapter07/validated/Coordinate.java
如果没有完全定义我们的注释(@NoQuadrantIII注释),这个类将无法编译;很快就会出现。
让我们看一下我们的测试代码,它创建了九个坐标并全部持久化;代表原点以及象限 I、II 和 IV 的Coordinate对象都应该成功保存, 17 和象限 III 的坐标应该失败。我们将再次使用数据提供者机制来消除大量的重复代码,但不是针对失败情况,这在范围上是有限的。这一次,我们将明确测试故障条件。
package chapter07.validator;
import chapter07.validated.Coordinate;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import javax.validation.ConstraintViolationException;
public class CoordinateTest {
private void persist(Coordinate entity) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
session.persist(entity);
tx.commit();
}
}
@DataProvider(name = "validCoordinates")
private Object[][] validCoordinates() {
return new Object[][]{
{1, 1},
{-1, 1},
{1, -1},
{1, 0},
{-1, 0},
{0, -1},
{0, 1},
{0, 0},
// trailing comma is valid: see JLS 10.6 https://bit.ly/3C3QN0J
};
}
@Test(dataProvider = "validCoordinates")
public void testValidCoordinate(Integer x, Integer y) {
Coordinate c = Coordinate.builder().x(x).y(y).build();
persist(c);
// has passed validation, if we reach this point.
}
@Test(expectedExceptions = ConstraintViolationException.class)
public void testInvalidCoordinate() {
testValidCoordinate(-1, -1);
}
}
Listing 7-16src/test/java/chapter07/validator/CoordinateTest.java
创建验证约束涉及两个类:一个是注释本身,另一个是注释的实现。
package chapter07.validated;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class QuadrantIIIValidator
implements ConstraintValidator<NoQuadrantIII, Coordinate> {
@Override
public void initialize(NoQuadrantIII constraintAnnotation) {
}
@Override
public boolean isValid(
Coordinate value,
ConstraintValidatorContext context
) {
return !(value.getX() < 0 && value.getY() < 0);
}
}
Listing 7-18src/main/java/chapter07/validated/QuadrantIIIValidator.java
package chapter07.validated;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {QuadrantIIIValidator.class})
@Documented
public @interface NoQuadrantIII {
String message() default "Failed quadrant III test";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Listing 7-17src/main/java/chapter07/validated/NoQuadrantIII.java
在这种情况下,isValid()方法——它得到一个ConstraintValidatorContext和一个Coordinate进行验证——我们可以简单地使用Coordinate并检查它的属性,看看它是否通过验证。可能存在更复杂的情况;例如,注释可以包括要使用的值的范围。
摘要
本章介绍了标准 Java 持久化 API 配置文件的使用,以及如何访问持久化生命周期和持久化之前的验证。它还讨论了使用 Lombok 来帮助避免样板代码,并展示了如何在 TestNG 中使用数据提供者来消除额外的测试代码。
在下一章中,我们将看看客户端应用如何通过使用Session对象与实体的数据库表示进行通信。
八、使用会话
您可能已经注意到,Session是访问 Hibernate 功能的中心点。我们现在来看看它体现了什么,以及它暗示了你应该如何使用它。
会话
从前面章节的例子中,你会注意到一小部分类主导了我们与 Hibernate 的交互。其中,Session,实际上是一个接口,是关键。
Session对象用于创建新的数据库实体,从数据库中读入对象,更新数据库中的对象,以及从数据库中删除对象。 1 它允许您管理数据库访问的事务边界,并(在必要时)获得一个传统的 JDBC 连接对象,以便您可以对数据库做一些 Hibernate 开发人员在他们现有的设计中没有考虑到的事情。
如果您熟悉 JDBC 方法,这有助于将Session对象想象成 JDBC 连接,将提供会话对象的SessionFactory想象成提供Connection对象的连接池。图 8-1 展示了这些角色的相似之处。

图 8-1
会话和 JDBC 连接之间的相似性
SessionFactory物为贵物;不必要的重复会很快导致问题,创建它们是一个相对耗时的过程。理想情况下,您的应用将要访问的每个数据库都应该有一个单独的SessionFactory。
对象也是线程安全的,所以没有必要为每个线程获取一个。然而,您将创建大量的Session对象——至少每个使用 Hibernate 的线程一个。Hibernate 中的Sessions是而不是线程安全的,所以线程间共享Session对象可能会导致数据丢失或死锁。事实上,即使在一个特定线程的生命周期中,您也会经常想要创建多个Session实例(参见“线程”一节中的并发问题)。
Hibernate 会话和 JDBC 连接之间的类比仅限于此。一个重要的区别是,如果 Hibernate Session对象抛出任何类型的异常,您必须丢弃它并获得一个新的Session。这可以防止会话缓存中的数据与数据库不一致。
我们已经在第四章中介绍了核心方法,所以我们不会讨论所有通过Session接口可用的方法。要全面了解可用的 API,您应该阅读 Hibernate 网站或 Hibernate 6 下载中的 API 文档。表 8-1 到 8-4 给出了您可用的各种方法的概述;尽管篇幅很长,但这并不是一个详尽的列表。
表 8-4
Session与 JDBC 连接相关的方法
方法
|
描述
|
| --- | --- |
| connection() | 检索对基础数据库连接的引用。 |
| disconnect() | 断开基础数据库连接。 |
| reconnect() | 重新连接基础数据库连接。 |
| isConnected() | 确定基础数据库连接是否已连接。 |
表 8-3
Session资源管理方法
方法
|
描述
|
| --- | --- |
| contains() | 确定特定对象是否与数据库相关联。 |
| clear() | 清除所有已加载实例的会话,并取消任何尚未完成的保存、更新或删除操作。保留所有正在使用的迭代器。 |
| evict() | 取消对象与会话的关联,以便不会保留对其进行的后续更改。 |
| flush() | 将所有挂起的更改刷新到数据库中-将执行所有保存、更新和删除操作。本质上,这将使会话与数据库同步。然而,这仍然发生在事务的上下文中,因此它的有用性可能会受到所使用的事务种类的限制。 |
| isOpen() | 确定会话是否已关闭。 |
| isDirty() | 确定会话是否与数据库同步;如果会话没有将内存中的更改写入数据库表,则为true。 |
| getCacheMode() | 确定当前使用的缓存模式。 |
| setCacheMode() | 更改当前使用的缓存模式。 |
| getCurrentLockMode() | 确定特定对象当前采用的锁定模式。(可使用lock()方法进行设置,例如,在许多其他选项中。) |
| setFlushMode() | 确定当前使用的冲洗方法。选项包括每次操作后刷新、需要时刷新、从不刷新或仅在提交时刷新。 |
| setReadOnly() | 将持久对象标记为只读(或可写)。将一个对象标记为只读会带来一些性能上的好处,但是在将其标记为可写之前,对其状态的更改将被忽略。 |
| close() | 关闭会话,并因此关闭基础数据库连接;释放其他资源(如缓存)。调用close()后,不得对Session对象执行操作。 |
| getSessionFactory() | 检索对创建当前Session实例的SessionFactory对象的引用。 |
表 8-2
Session事务和锁定的方法
方法
|
描述
|
| --- | --- |
| beginTransaction() | 开始事务。 |
| getTransaction() | 检索当前事务对象。当没有事务正在进行时,这不会返回null。而是返回对象的active属性为false。 |
| lock() | 获取一个对象的数据库锁(或者可以像merge()一样使用,如果给定了LockMode.NONE)。实际上,该方法检查数据库中的对象与内存中的对象相比的状态。 |
表 8-1
Session创建、读取、更新、删除的方法
方法
|
描述
|
| --- | --- |
| save() | 将对象保存到数据库。对于已经保存到数据库中的对象,不应调用此方法。 |
| saveOrUpdate() | 将对象保存到数据库,如果对象已经存在,则更新数据库。这种方法比save()方法效率稍低,因为它可能需要执行一个SELECT语句来检查对象是否已经存在,但是如果对象已经被保存,它不会失败。 |
| merge() | 将非持久化对象的字段合并到适当的持久化对象中(由 ID 决定)。如果数据库中不存在这样的对象,则创建并保存一个。 |
| persist() | 将对象与会话重新关联,以便持久保存对对象所做的更改。 |
| get() | 通过对象的标识符从数据库中检索特定对象。 |
| getEntityName() | 检索实体名称(这通常与 POJO 的完全限定类名相同)。 |
| getIdentifier() | 确定与会话关联的特定对象的标识符(表示主键的对象)。 |
| load() | 通过对象的标识符从数据库加载一个对象(如果您不确定该对象是否在数据库中,并且您不想捕获一个异常,那么您应该使用get()方法)。 |
| refresh() | 刷新数据库中关联对象的状态。 |
| update() | 用对对象的更改更新数据库。 |
| delete() | 从数据库中删除对象。 |
| createFilter() | 创建过滤器(选择标准)以缩小数据库操作的范围。 |
| enableFilter() | 在createFilter()生成的查询中启用命名过滤器。 |
| disableFilter() | 禁用命名过滤器。 |
| getEnabledFilter() | 检索当前启用的筛选器对象。 |
| createQuery() | 创建要应用于数据库的 Hibernate 查询。 |
| getNamedQuery() | 从映射文件中检索查询。 |
| cancelQuery() | 取消另一个线程中当前正在进行的任何查询的执行。这不一定规定释放什么资源或何时释放;例如,尽管取消了查询,数据库仍可能尝试完成查询。 |
| createCriteria() | 创建用于缩小搜索结果范围的 criteria 对象。 |
事务和锁定
事务和锁定密切相关:为执行事务而选择的锁定技术可以决定事务的性能和成功的可能性。所选的事务类型在某种程度上决定了它必须使用的锁定类型。
如果事务不符合您的需求,您没有义务使用它们,但是很少有好的理由来避免它们。如果您决定避免它们,您将需要在适当的时候调用会话中的flush()方法,以确保您的更改被持久化到数据库中。
不要回避事务。获取一个事务只需要很少的代码,我们已经在前面章节的SessionUtil中看到了使用 lambdas 管理带有活动Session和Transaction的操作的例子——知道事情何时以及如何发生的好处不能被夸大。值得重复的是:只使用事务。
处理
事务是一个工作单元,保证其行为就像您独占使用数据库一样。一般来说,如果你把工作包装在一个事务中,其他系统用户的行为不会影响你的数据。 2 一个事务可以被启动,提交将数据写入数据库,或者回滚以删除从头开始的所有更改(通常是错误的结果)。为了正确地完成一个操作,您从数据库获得一个Transaction对象(开始事务)并操纵会话,如下面的代码所示:
try(Session session = factory.openSession()) {
session.beginTransaction();
// Normal session usage here?
session.getTransaction().commit();
} catch (HibernateException e) {
Transaction tx = session.getTransaction();
if (tx.isActive()) tx.rollback();
}
在现实世界中,并不希望所有的事务都是完全 ACID 的(参见下一节!)因为这会导致性能问题。
不同的数据库供应商支持并允许您或多或少地违反 ACID 规则,但是对隔离规则的控制程度实际上是由 SQL-92 标准规定的。有一些重要的原因让你想打破这个规则,所以 JDBC 和 Hibernate 都明确地考虑到了这一点。
酸性测试
ACID 是一个经常与数据库联系在一起的缩写词,代表一个事务所代表的四个属性。分别是原子性、一致性、隔离性和耐久性:
-
原子性:一个事务应该要么全部要么什么都没有。如果未能完成,数据库将保持原样,就像从未执行过任何操作一样——这被称为
rollback。原子性意味着你不能只获得事务提交数据的一部分;事务中的更改作为一个单元应用。 -
一致性:事务应该不会违反为数据库定义的任何规则。例如,必须遵守外键约束。如果由于某种原因这是不可能的(例如,您试图持久化与模式不一致的数据),事务将被回滚。
-
隔离:在事务成功完成之前,该事务的影响对所有其他事务都是完全不可见的。这保证了事务将总是看到处于合理状态的数据。例如,考虑对用户地址的更新是否应该只包含正确的地址(即,它永远不会有一个位置的房屋名称,而是另一个位置的邮政编码);如果没有隔离,一个事务可以很容易地看到另一个事务何时更新了第一部分但尚未完成。
-
持久化:数据应该保持完整。如果系统由于任何原因出现故障,应该总是可以检索到故障发生前的数据库。
表 8-5 中列出了 JDBC(和休眠)允许的隔离级别。
表 8-5
JDBC 隔离级别
|水平
|
名字
|
事务行为
|
| --- | --- | --- |
| Zero | 没有人 | 任何事情都是允许的;数据库或驱动程序不支持事务。 |
| one | 未提交读取 | 允许脏的、不可重复的和幻像读取。 |
| Two | 已提交读取 | 允许不可重复的读取和幻像读取。 |
| four | 可重复读 | 允许幻像读取。 |
| eight | 可序列化 | 这条规则必须绝对遵守。 |
脏读可以看到未提交事务的进行中的改变。与 ACID 列表中讨论的隔离示例一样,它可能会看到地址的错误邮政编码。
一个不可重复的 read 可以随着时间的推移看到相同查询的不同数据。例如,它可能在事务开始时确定特定用户的邮政编码,并在事务结束时再次确定邮政编码,然后两次都得到不同的答案,而不进行任何更新。
一个幻影读取看到相同查询的不同行数。例如,它可能在查询开始时看到数据库中有 100 个用户,在查询结束时看到 105 个用户,而没有进行任何更新。
Hibernate 将隔离视为一个全局设置:您以通常的方式应用配置选项hibernate.connection.isolation,将其设置为表 8-5 中允许的值之一。
锁
数据库可以以多种方式符合这些不同的隔离级别,并且您将需要锁的工作知识,以便在所有情况下从您的应用中获得期望的行为和性能。
为了防止同时访问数据,数据库本身将获取该数据的锁。这可以仅在对数据进行瞬时操作时获取,也可以保留到事务结束。前者叫做乐观锁定,后者叫做悲观锁定。
Read Uncommitted 隔离级别总是获取乐观锁,而 Serializable 隔离级别将只获取悲观锁。一些数据库提供了一个特性,允许您将FOR UPDATE查询附加到一个选择操作,这需要数据库获得一个悲观锁,即使在较低的隔离级别。
Hibernate 在这个特性可用的时候提供了一些支持,并通过添加描述从 Hibernate 自己的缓存中获得的额外隔离程度的工具来进一步发展这个特性。
LockMode枚举??控制这种细粒度的隔离(见表 8-6 )。它只适用于get()方法,所以它是有限的;然而,如果可能的话,最好是直接控制前面提到的隔离。
表 8-6
可以请求的锁定模式
|方式
|
描述
|
| --- | --- |
| NONE | 仅当缓存中的对象不可用时,才从数据库中读取。 |
| READ | 从数据库中读取,而不考虑缓存中的内容。 |
| UPGRADE | 为要访问的数据获取特定于方言的升级锁(如果数据库中有这种锁的话)。 |
| UPGRADE_NOWAIT | 行为类似于UPGRADE,但是当数据库和方言提供支持时,该方法将立即失败并出现异常。如果没有该选项,或者在不支持该选项的数据库上,查询必须等待锁被授予(或者等待超时)。 |
当 Hibernate 写入当前事务中的某一行时,它会自动获得另一个锁模式WRITE。这个模式不能被显式设置,但是调用getLockMode()可能会返回它。
讨论了锁定的一般情况后,我们需要触及锁可能导致的一些问题。
僵局
当两个资源争用依赖关系而没有解决方案时,就会发生死锁。例如,假设您有两个需要资源“A”和“B”的进程——只不过第一个进程先获取资源 A,然后访问 B,第二个进程先获取资源 B,然后加载 A。如果第一个进程获取 A,然后等待访问 B,但第二个进程在进程 A 获取它之前加载 B,则它们在尝试获取第二个资源时会死锁。
它看起来像这样:
|流程一
|
流程二
|
| --- | --- |
| 锁定资源 A | |
| | 锁定资源 B |
| | 等待,直到有可用的 |
| 等到 B 可用 | |
Hibernate 可以检测到这种循环,如果发现,它将抛出一个错误(一个OptimisticLockException,因为我们依赖乐观锁)。让我们创建一个,这样我们就可以看到发生了什么。我们的例子将把两个Runnable实例提交到一个ServiceExecutor中,每个实例将获得(并修改,因此锁定)两个资源,除了顺序不同,因此造成了我们的死锁情况。之后,它将通过确定数据是否回到其原始(未修改)状态来验证两个事务都失败了。
首先,我们当然需要我们的项目模型。
这个项目中有一些元素适用于缓存部分,我们将在本章后面讨论。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hibernate-6-parent</artifactId>
<groupId>com.autumncode.books.hibernate</groupId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter08</artifactId>
<dependencies>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>util</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-core</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
</dependency>
</dependencies>
</project>
Listing 8-1chapter08/pom.xml
接下来,我们需要一个合作实体。
package chapter08.model;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Data
public class Publisher {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
String name;
}
Listing 8-2chapter08/src/main/java/chapter08/model/Publisher.java
这是 Hibernate 配置文件。注意,它和pom.xml一样,有一些与缓存相关的东西;这些将在本章后面使用。
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:./db8</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<property name="hibernate.cache.region.factory_class">
jcache
</property>
<property name="hibernate.javax.cache.missing_cache_strategy">
create
</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<property name="use_sql_comments">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create-drop</property>
<mapping class="chapter08.model.Publisher"/>
</session-factory>
</hibernate-configuration>
Listing 8-3chapter08/src/test/resources/hibernate.cfg.xml
最后我们来看死锁例子本身。它看起来很长,但是大部分的复杂性在于试图确保更新在正确的时间以正确的顺序发生。让我们看一下代码,然后解包。
package chapter08;
import chapter08.model.Publisher;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.PessimisticLockException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory
;
import org.testng.annotations.Test;
import javax.persistence.OptimisticLockException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.testng.Assert.assertEquals;
public class DeadlockExample {
Logger logger= LoggerFactory.getLogger(this.getClass());
private Long createPublisher(Session session, String name) {
Publisher publisher = new Publisher();
publisher.setName(name);
session.save(publisher);
return publisher.getId();
}
private void updatePublishers(String prefix, Long... ids) {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
for (Long id : ids) {
Thread.sleep(300);
Publisher publisher = session
.byId(Publisher.class)
.load(id);
publisher.setName(prefix + " " + publisher.getName());
}
tx.commit();
} catch (OptimisticLockException e) {
logger.error("lock exception with prefix "+ prefix);
} catch(InterruptedException ignored) {
}
}
@Test
public void showDeadlock() throws InterruptedException {
Long publisherAId;
Long publisherBId;
//clear out old data and populate tables
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
session
.createQuery("delete from Publisher")
.executeUpdate();
publisherAId = createPublisher(session, "A");
publisherBId = createPublisher(session, "B");
tx.commit();
}
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(
() -> updatePublishers("session1", publisherAId, publisherBId));
executor.submit(
() -> updatePublishers("session2", publisherBId, publisherAId));
executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.out.println("Executor did not terminate");
}
}
try (Session session = SessionUtil.getSession()) {
Query<Publisher> query = session.createQuery(
"from Publisher p order by p.name",
Publisher.class
);
String result = query
.list()
.stream()
.map(Publisher::getName)
.collect(Collectors.joining(","));
assertEquals(result, "A,B");
}
}
}
Listing 8-4chapter08/src/test/java/chapter08/DeadlockExample.java
我们看到的第一个方法是createPublisher(),它接受一个活动的Session和一个“发布者名称”——并返回它刚刚为我们保存的Publisher的一个id。当我们试图更新Publisher时,我们将使用该标识符来锁定它。
第二种方法是updatePublishers(),它需要一个“前缀”和一个键列表来更新。这个方法的要点在这个测试的上下文之外是无意义的;只需在每个标识符被传入的Publisher的名字前加上prefix,按顺序加上的,每次更新之间有相当长的延迟。我们将从两个不同的线程中执行这个方法,每个线程都有不同的标识符顺序,这就造成了我们的死锁情况。
它甚至告诉我们它何时得到锁异常——“以防万一”
为什么延迟这么久?主要是因为你不能保证遗嘱执行人什么时候会真正开始执行。在大多数机器上,300 毫秒的延迟是不必要的;但是,请注意,您的 CPU 可能不同,您可能需要调整延迟。
最后,我们来看看实际的测试本身。它有三个阶段:第一阶段建立我们的数据(使用createPublisher()方法);第二个创建一个ExecutorService并提交两个任务,由updatePublishers()执行,颠倒标识符的顺序:第一个执行程序更新第一个,然后更新第二个Publisher,另一个执行程序更新第二个,然后更新第一个Publisher,这会创建冲突的更新。
*showDeadlock()的最后一个阶段检索一组Publisher实体,按照name对它们进行排序,这样结果是可预测的,并验证名称分别是“A”和“B”,这是我们第一次创建它们时设置的名称。
请记住,我们希望它们保持不变,因为我们故意制造了一个死锁情况,这样两组更新都会失败。
在输出中,连同任何其他日志记录信息,我们应该看到以下消息,尽管它们的顺序可能因您而异(我还从消息中截取了时间戳,因为它们是不相关的):
[pool-1-thread-2] ERROR chapter08.DeadlockExample - lock exception with prefix session2
[pool-1-thread-1] ERROR chapter08.DeadlockExample - lock exception with prefix session1
贮藏
访问数据库是一个昂贵的 4 操作,即使是简单的查询。请求必须被发送(通常通过网络)到服务器。数据库服务器可能必须将 SQL 编译成查询计划。查询计划必须运行,并且很大程度上受到磁盘性能的限制。产生的数据必须被传送回(同样,通常通过网络T5到客户端,然后应用才能开始处理结果。
如果查询运行多次,大多数好的数据库都会缓存查询结果,从而消除磁盘 I/O 和查询编译时间。但是,如果有大量的客户端发出完全不同的请求,那么这种方法的价值将是有限的。即使高速缓存通常保存结果,通过网络传输信息所花费的时间通常也是延迟的主要部分。
一些应用将能够利用进程内数据库,但这是例外而不是规则——这种数据库有其自身的局限性。
自然而明显的答案是在数据库连接的客户端有一个缓存。这不是 JDBC 直接提供或支持的特性,但是 Hibernate 提供了一个缓存(一级,或 L1 缓存),所有请求都必须通过它。二级缓存(L2)是可选和可配置的。
L1 缓存确保在一个会话中,对数据库中给定对象的请求总是返回相同的对象实例,从而防止数据冲突,并防止 Hibernate 多次尝试加载一个对象。
L1 缓存中的项目可以通过在会话中为您希望丢弃的对象调用evict()方法来单独丢弃。要丢弃 L1 缓存中的所有项目,调用clear()方法。
通过这种方式,Hibernate 比传统的 JDBC 方法有一个主要的优势:不需要开发人员做额外的工作,Hibernate 应用就可以获得客户端数据库缓存的好处。
图 8-2 显示了会话可用的两个缓存:强制的 L1 缓存,所有请求都必须通过它,以及可选的 L2 缓存。在尝试在 L2 缓存中定位对象之前,将始终查询 L1 缓存。您会注意到 L2 缓存在 Hibernate 的外部;尽管它是以对 Hibernate 用户透明的方式通过会话访问的,但它是各种缓存的可插拔接口,这些缓存与 Hibernate 应用维护在同一个 JVM 上,或者维护在外部 JVM 上。这允许在同一台机器上的应用之间或者甚至在多台机器上的多个应用之间共享缓存。

图 8-2
Session和缓存的关系
原则上,任何第三方缓存都可以和 Hibernate 一起使用。提供了一个org.hibernate.Cache接口,必须实现该接口才能为 Hibernate 提供缓存实现的句柄。然后,通过将实现类名作为hibernate.cache.provider_class属性的值来指定缓存提供者。
对于 Hibernate 6,首选的缓存机制是使用 JCache 兼容的提供者。JCache 是管理缓存应该提供的最少功能的规范(就像 JPA 是管理持久化框架应该提供的最少功能的规范一样)。有很多兼容 JCache 的库(?? );已知提供商列表见 https://jcp.org/aboutJava/communityprocess/implementations/jsr107/index.html 。
通过选择一个CacheMode枚举(参见表 8-7 )并使用Session.setCacheMode()方法应用它,可以在每个会话的基础上配置对 L2 缓存的访问类型。
表 8-7
缓存模式选项
|方式
|
描述
|
| --- | --- |
| NORMAL | 根据需要从缓存中读取数据,并将其写入缓存。 |
| GET | 数据永远不会添加到缓存中(尽管缓存条目在被会话更新时会失效)。 |
| PUT | 永远不会从缓存中读取数据,但是当会话从数据库中读取缓存条目时,缓存条目会被更新。 |
| REFRESH | 这与 PUT 相同,但是如果已经设置了 use_minimal_puts Hibernate 配置选项,它将被忽略。 |
| IGNORE | 数据永远不会从缓存中读取或写入缓存(除非缓存条目在被会话更新时仍然无效,以防另一个Session以某种方式缓存了它们)。 |
CacheMode设置不影响 L1 缓存的访问方式。
使用 L2 缓存的决定并不明确。虽然它有可能大大减少对数据库的访问,但好处取决于缓存的类型和访问方式。
分布式缓存会导致额外的网络流量。某些类型的数据库访问可能会导致缓存内容在使用前被刷新;在这种情况下,它会给事务增加不必要的开销。
L2 缓存无法解释底层数据中的更改,这些更改是不支持缓存的外部程序的操作的结果。这可能会导致陈旧数据的问题,而这不是 L1 缓存的问题。
实际上,与大多数优化问题一样,最好在真实的负载条件下进行性能测试。这将让您确定是否需要缓存,并帮助您选择哪一个将提供最大的改进。
实际上,配置缓存使用非常简单。在本例中,为了设置好一切,我们需要执行以下操作:
-
选择一个缓存提供者并将依赖项添加到 Maven。
-
将 Hibernate 配置为将缓存提供者用于二级缓存。
-
改变我们的实体,将它们标记为可缓存的。
我们将选择 Apache Ignite 作为缓存提供者,因为在 Java SE 环境中设置它很简单。Maven 的依赖块如下所示: 6
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-core</artifactId>
<version>2.10.0</version>
</dependency>
我们可以通过在配置中添加一些属性来告诉 Hibernate 使用我们的二级缓存,正如我们在本章前面已经看到的:
<property
name="hibernate.cache.region.factory_class">
jcache
</property>
<property
name="hibernate.javax.cache.missing_cache_strategy">
create
</property>
我们需要做的最后一件事是将实体标记为可缓存的。在清单 8-5 中,我们将创建一个简单的Supplier实体(我们将在接下来的章节中再次讨论)并将其标记为二级缓存的候选对象。
package chapter08.model;
import lombok.Data;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import javax.persistence.*;
import java.io.Serializable;
@Entity
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Data
public class Supplier implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@Column(unique = true)
String name;
public Supplier(String name) {
this.name = name;
}
public Supplier() {
}
}
Listing 8-5chapter08/src/main/java/chapter08/model/Supplier.java
如果我们在一个会话中加载一个特定的Supplier,然后在另一个会话中立即加载相同的Supplier,数据库将不(必然)被查询,因为每次都是从二级缓存中而不是从数据库中提取。使用不同的会话是必要的,因为Supplier实例将被缓存在每个Session的一级缓存中;会话共享二级缓存,而不是一级缓存。
让我们通过另一个测试来展示这一点。
package chapter08;
import chapter08.model.Supplier;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.List;
public class QueryTest {
List<Integer> keys = new ArrayList<>();
@BeforeMethod
public void populateData() {
clearSuppliers();
Session session = SessionUtil.getSession();
Transaction tx = session.beginTransaction();
for (int i = 0; i < 10; i++) {
Supplier supplier = new Supplier("Supplier " + (i + 1));
session.save(supplier);
keys.add(supplier.getId());
}
tx.commit();
session.close();
}
@AfterMethod
public void clearSuppliers() {
Session session = SessionUtil.getSession();
Transaction tx = session.beginTransaction();
session.createQuery("delete from Supplier")
.executeUpdate();
tx.commit();
session.close();
}
@Test
public void testSuppliers() {
for(int i=0;i<100; i++) {
// create a new Session every loop...
try(Session session=SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Integer key=keys.get((int)(Math.random()*keys.size()));
Supplier supplier = session.get(Supplier.class,key);
System.out.println(supplier.getName());
tx.commit();
}
}
}
}
Listing 8-6chapter08/src/test/java/chapter08/QueryTest.java
这里,我们通过在数据库中创建一组Supplier实例来开始我们的测试。(当然,我们有很多方法可以做到这一点,但这非常简单。)
实际的测试本身—testSuppliers—只是使用一个随机键对我们的一组Supplier实例进行大量加载。然后它打印出每次装载的供应商名称,因此它将生成一个相当长的输出。如果您运行这个测试,输出看起来像这样,因为我们在hibernate.cfg.xml的配置中打开了“显示 SQL”:
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 9
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 2
Supplier 2
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 7
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 5
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 6
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 10
Supplier 7
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 1
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 8
Supplier 10
Supplier 9
您会注意到数据库实际上并不经常被查询;我们将看到发出十条 SQL 语句(过一会儿,因为这不是输出的完整运行),带有一批重复的供应商名称;这是因为会话正在从 Ignite 缓存中加载供应商。
这个有用吗?很难说。在这种情况下,它对于演示缓存的功能当然是有用的,但是这真的是性能改进吗?不够重要;对于我们的测试来说,生成控制台输出是整个测试中最昂贵的部分,缓存为我们节省的很少。对于一个真正的应用来说,建议是总是在真实的读写条件下进行彻底的测试,并测量不同配置的结果。
二级缓存可以显著提高性能,但前提是条件合适并且应用得当。
线
考虑了 Hibernate 应用可用的缓存后,现在您可能会担心如果两个执行线程争用 Hibernate 会话缓存中的同一个对象,会有传统 Java 死锁的风险。
原则上,这是可能的,与数据库死锁不同,Java 线程死锁不会超时并显示错误消息。幸运的是,有一个非常简单的解决方案:
Patient: Doctor, it hurts when I do this.
Doctor: Don’t do that, then.
不要在线程间共享Session对象。这将消除会话缓存中包含的对象死锁的风险。
确保不在当前线程外使用同一个Session对象的最简单方法是使用当前方法的本地实例,或者创建一个会话并将其传递给多个“工作方法”,当操作完成时关闭会话:
try(Session session=SessionUtil.getSession()) {
Transaction tx=session.beginTransaction();
operationOne(session);
operationTwo(session);
operationThree(session);
tx.commit();
}
如果您必须在更长的时间内维护一个实例,那么就在一个ThreadLocal对象中维护这个实例。然而,在大多数情况下,Session对象的轻量级特性使得构造、使用和销毁一个实例比存储一个会话更实际。
摘要
在这一章中,我们已经讨论了Session对象的性质以及如何使用它们来获取和管理事务。我们已经了解了应用可用的两个级别的缓存,以及并发线程应该如何管理会话。
在下一章,我们将讨论从数据库中检索对象的各种方法。我们还将向您展示如何使用 HQL 对数据库执行更复杂的查询。
*九、搜索和查询
在上一章中,我们讨论了如何使用 Hibernate 会话与数据库进行交互。一些会话的方法在其参数列表中接受查询字符串或返回Query对象。这些方法用于从数据库请求任意信息。为了充分展示它们是如何被使用的,我们必须引入 Hibernate 查询语言(HQL ),用于表述这些请求。除了提取信息(用SELECT),HQL 还可以用来修改数据库中的信息(用INSERT、UPDATE和DELETE)。我们将在本章中介绍所有这些基本功能。
HQL 是一种面向对象的查询语言,类似于 SQL,但 HQL 不是对表和列进行操作,而是处理持久对象及其属性。它是 JPQL 的超集,JPQL 是 Java 持久化查询语言;JPQL 查询是有效的 HQL 查询,但不是所有的 HQL 查询都是有效的 JPQL 查询。
HQL 是一种有自己句法和语法的语言。HQL 查询的表达方式很像 SQL 本身,像from Product p一样是一个字符串。最终,Hibernate 会将您的 HQL 查询翻译成传统的 SQL 查询;Hibernate 还提供了一个 API,允许您直接发出 SQL 查询。
Hibernate 查询语言(HQL)
虽然大多数 ORM 工具和对象数据库都提供了对象查询语言,但 Hibernate 的 HQL 以其完整和易用而脱颖而出。虽然您可以在 Hibernate 中直接使用 SQL 语句(这将在本章的“使用原生 SQL”一节中详细介绍),但我们建议您尽可能使用 HQL(或标准),以避免数据库可移植性的麻烦,并利用 Hibernate 的 SQL 生成和缓存策略。除了相对于传统 SQL 的技术优势,HQL 是一种比 SQL 更紧凑的查询语言,因为它可以利用 Hibernate 映射中定义的关系信息。
我们意识到,并不是每个开发人员都相信 Hibernate 生成的 SQL 是经过完美优化的。如果您在查询中遇到性能瓶颈,我们建议您在关键组件的性能测试期间对数据库使用 SQL 跟踪。如果您发现某个领域需要优化,首先尝试使用 HQL 进行优化,然后再使用原生 SQL。Hibernate 通过 JMX MBean 提供统计信息,您可以用它来分析 Hibernate 的性能。Hibernate 的统计数据还可以让您深入了解缓存的性能。
如果您想通过基于 GUI 的工具执行 HQL 语句,Hibernate 团队在 Hibernate Tools 子项目中为 Eclipse 提供了一个 Hibernate 控制台。这个控制台是 Eclipse 最新版本的插件;更多信息见 https://tools.jboss.org/ 。其他 ide 也有类似的功能。
语法基础
HQL 受到了 SQL 的启发,也是 Java 持久化查询语言(JPQL)的主要灵感来源。JPQL 规范包含在 Java 社区过程网站( www.jcp.org/en/jsr/detail?id=338 )提供的 JPA 标准中。HQL 的句法被定义为一种反语法;语法文件包含在 Hibernate 核心下载的语法目录中。(ANTLR 是一个构建语言解析器的工具。)
由于 ANTLR 语法文件有些晦涩,而且根据 ANTLR 语法的规则,并不是每一个允许的语句都可以在 Hibernate 中使用,所以我们在本节中概述了四个基本 HQL 操作的语法。注意,下面对语法的描述并不全面;有一些不推荐使用的或者更晦涩的用法(特别是对于SELECT语句)没有在这里讨论。
更新
UPDATE改变数据库中现有对象的详细信息。这是一个从内存到数据库的操作——更新不会影响你已经加载的任何东西。(当然,您可以更新已加载的对象,当提交事务时,任何更改都将被写入数据库;然而,这并不需要 HQL UPDATE。)下面是UPDATE语句的语法:
UPDATE [VERSIONED]
[FROM] path [[AS] alias] [, ...]
SET property = value [, ...]
[WHERE logicalExpression]
“路径”是指一个或多个实体的完全限定名。别名可用于缩写对特定实体或其属性的引用,并且必须在查询中的属性名称不明确时使用。
VERSIONED表示更新将更新时间戳,如果有的话,时间戳是被更新实体的一部分。
属性名称是“从”路径中列出的实体的属性名称。
逻辑表达式的语法将在后面的“对 HQL 使用限制”一节中讨论。
实际更新的示例可能如下所示:
Query query=session.createQuery(
"update Person p set p.creditscore=:creditscore where p.name=:name"
);
query.setInteger("creditscore", 612);
query.setString("name", "John Q. Public");
int modifications=query.executeUpdate();
Query正常输入;实际类型是Query<R>,我们在这里没有显示R!没关系,因为我们发布的是UPDATE,而不是选择;executeUpdate()调用返回修改记录的计数,我们丢弃了可能从list()操作返回的类型。
删除
DELETE从数据库中删除现有对象的详细信息。与更新一样,你已经加载的对象不受 HQL DELETE的影响。这也意味着,对于使用 HQL 执行的删除,将不遵循 Hibernate 的级联规则。但是,如果您在数据库级别指定了级联删除(直接或通过 Hibernate),数据库仍然会删除子行。这种删除方法通常称为“批量删除”,因为这是从数据库中删除大量实体的最有效方法。下面是DELETE语句的语法:
DELETE
[FROM] path [[AS] alias]
[WHERE logicalExpression]
与UPDATE一样,path是实体的完全限定名。别名可用于缩写对特定实体或其属性的引用,并且必须在查询中的属性名称不明确时使用。
实际上,删除可能如下所示:
Query query=session.createQuery(
"delete from Person p where p.accountstatus=:status
");
query.setString("status", "toBePurged");
int rowsDeleted=query.executeUpdate();
和UPDATE的例子一样,我们放弃了可能已经用Query指定的类型,因为我们只对删除的实体数量感兴趣;我们没有使用类型化查询。
插入
HQL INSERT不能用于直接插入任意实体——它只能用于插入从SELECT查询中获得的信息构建的实体(不像普通 SQL,在普通 SQL 中,INSERT命令可以用于将任意数据插入表中,以及插入从其他表中选择的值)。基本上,HQL INSERT使用来自数据库的数据来构建实体,而不是使用提供给它的数据。下面是INSERT语句的语法:
INSERT
INTO path ( property [, ...])
select
实体的名称由path表示。属性名是在合并的SELECT查询的FROM路径中列出的实体的属性名。
选择查询是一个 HQL SELECT查询(如下一节所述)。
由于该 HQL 语句只能使用 HQL select 提供的数据,因此其应用可能会受到限制。假设我们想把要删除的记录从一个USERS表复制到一个PURGED_USERS表,用于存档目的。 1 我们可以手工将USERS表中的记录复制到PURGED_USERS表中,这样就满足了INSERT对 HQL 的要求,比如:
Query query=session.createQuery(
"insert into purged_users(id, name, status) "+
"select id, name, status from users where status=:status"
);
query.setString("status", "toBePurged");
int rowsCopied=query.executeUpdate();
与 HQL 的UPDATE和DELETE一样,我们忽略了Query的类型,因为我们不要求返回类型化的信息,只要求返回修改的行数。
挑选
HQL SELECT用于查询数据库中的类及其属性。如前所述,这是对 HQL SELECT查询全部表达能力的简短总结。下面是SELECT语句的语法:
[SELECT [DISTINCT] property [, ...]]
FROM path [[AS] alias] [, ...] [FETCH ALL PROPERTIES]
WHERE logicalExpression
GROUP BY property [, ...]
HAVING logicalExpression
ORDER BY property [ASC | DESC] [, ...]
实体的全限定名是path。别名可用于缩写对特定实体或其属性的引用,并且必须在查询中使用的属性名称不明确时使用。
属性名是在FROM路径中列出的实体的属性名。
如果使用了FETCH ALL PROPERTIES,那么懒惰加载语义将被忽略,所有被检索对象的立即属性将被主动加载(这不适用于递归;以这种方式加载的实体可能会也可能不会检索到它们的嵌套数据)。
当列出的属性仅由FROM子句中的别名组成时,在 HQL 中可以省略SELECT子句。如果你在 JPQL 中使用 JPA,HQL 和 JPQL 的一个区别是 JPQL 中需要SELECT子句。
因此,在 HQL,使用“FROM USERS U”作为查询是可以接受的,而在 JPQL 中对应的是SELECT U FROM USERS U。
命名查询
Hibernate(和 JPA)提供了命名查询。命名查询是通过实体上的类级注释创建的;通常,查询应用于它们出现在源文件中的实体,但是没有绝对的要求这是真的。
命名查询是用@NamedQueries注释创建的,它包含一组@NamedQuery集合;每个都有查询内容(查询本身)和名称。
首先,让我们看一下项目模型,在这个框架下我们可以构建我们的项目。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hibernate-6-parent</artifactId>
<groupId>com.autumncode.books.hibernate</groupId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter09</artifactId>
<dependencies>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>util</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate.validator.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-cdi</artifactId>
<version>${hibernate.validator.version}</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>${javax.el-api.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>${javax.el-api.version}</version>
</dependency>
</dependencies>
</project>
Listing 9-1chapter09/pom.xml
接下来,让我们创建一个可以用作示例的对象模型。我们的对象模型将包含产品和供应商;它还将包含一个专门的产品(“软件”),为Product添加一个属性。我们在这里使用的层次结构的一个影响是 Lombok 不再像以前一样可用, 2 所以我们将从源代码中删除一些样板文件——即构造函数、赋值函数、访问函数、equals()、hashCode()和toString()。当然,本书的源代码下载将会包含所有这些方法。
package chapter09.model;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Objects;
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Product implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
Supplier supplier;
@Column
@NotNull
String name;
@Column
@NotNull
String description;
@Column
@NotNull
Double price;
public Product() {
}
public Product(Supplier supplier,
String name,
String description,
Double price) {
this.supplier = supplier;
this.name = name;
this.description = description;
this.price = price;
}
}
Listing 9-2chapter09/src/main/java/chapter09/model/Product.java
package chapter09.model;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Supplier implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@Column(unique = true)
@NotNull
String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true,
mappedBy = "supplier", targetEntity = Product.class)
List<Product> products = new ArrayList<>();
public Supplier(String name) {
this.name = name;
}
public Supplier() {
}
}
Listing 9-3chapter09/src/main/java/chapter09/model/Supplier.java
package chapter09.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Objects;
@Entity
public class Software extends Product implements Serializable {
@Column
@NotNull
String version;
public Software() {
}
public Software(Supplier supplier,
String name,
String description,
Double price,
String version) {
super(supplier, name, description, price);
this.version = version;
}
}
Listing 9-4chapter09/src/main/java/chapter09/model/Software.java
当然,我们还需要 Hibernate 配置。
<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:./db9</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- set up c3p0 for use -->
<property name="c3p0.max_size">10</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<property name="use_sql_comments">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create-drop</property>
<mapping class="chapter09.model.Software"/>
<mapping class="chapter09.model.Product"/>
<mapping class="chapter09.model.Supplier"/>
</session-factory>
</hibernate-configuration>
Listing 9-5chapter09/src/test/resources/hibernate.cfg.xml
添加一个命名查询就像向其中一个实体添加注释一样简单。例如,如果我们想要添加一个命名查询来检索所有的Supplier实体,我们可以通过向任何实体添加一个@NamedQuery注释来实现,尽管将查询放在Supplier的源代码中最有意义:
@NamedQuery(name = "supplier.findAll", query = "from Supplier s")
当然,对于这样一个简单的查询,根本不需要命名查询——您只需要使用查询文本。但是,为了保持一致性或者便于将来的维护,您可以使用命名查询。想象一下给Supplier添加一个字段,指示Supplier是否活动;然后,您可以轻松地更新查询以包含where active=true,而不必在代码中搜索查询Supplier集合的每个地方。
*通过添加一个@NamedQueries 注释,然后将命名查询作为该注释的一部分嵌入到一个数组中,可以将查询分组在一起。这将类似于清单 9-6 中所示的内容。
@NamedQueries({
@NamedQuery(name = "supplier.findAll",
query = "from Supplier s"),
@NamedQuery(name = "supplier.findByName",
query = "from Supplier s where s.name=:name"),
@NamedQuery(name = "supplier.averagePrice",
query = "select p.supplier.id, avg(p.price) " +
"from Product p " +
"GROUP BY p.supplier.id"),
})
@NamedNativeQueries({
@NamedNativeQuery(name = "supplier.findAverage",
query = "SELECT p.supplier_id, avg(p.price) "
+ "FROM Product p GROUP BY p.supplier_id"
)
})
Listing 9-6chapter09/src/main/java/chapter09/model/Supplier.java
敏锐的读者会看到@NamedNativeQueries在使用中。这将在后面解释,但是本地查询是通过 Hibernate 发出的 SQL 查询。如果你非常了解 SQL,并且你的目标是一个特定的数据库,那么本地查询是非常有用的;这里,我们实际上是使用简单的 SQL 从Product表构建一个投影。
使用命名查询非常简单。让我们创建一个为每个测试填充和清除数据的TestBase类,然后我们将创建一个使用我们的supplier.findAll查询的测试。??
使用TestBase非常简单,尽管它可能不是写过的最好的类;在运行测试之前,它填充一个数据集并初始化一个Session;测试结束后,关闭Session并进行清理。(它使用Session的方式主要是帮助缩短使用 TestBase 的类。)
package chapter09;
import chapter09.model.Product;
import chapter09.model.Software;
import chapter09.model.Supplier;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
public class TestBase {
Session session;
Transaction tx;
@BeforeMethod
public void populateData() {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
Supplier supplier = new Supplier("Hardware, Inc.");
supplier.getProducts().add(
new Product(supplier, "Optical Wheel Mouse", "Mouse", 5.00));
supplier.getProducts().add(
new Product(supplier, "Trackball Mouse", "Mouse", 22.00));
session.save(supplier);
supplier = new Supplier("Supplier 2");
supplier.getProducts().add(
new Software(supplier, "SuperDetect", "Antivirus", 14.95, "1.0"));
supplier.getProducts().add(
new Software(supplier, "Wildcat", "Browser", 19.95, "2.2"));
supplier.getProducts().add(
new Product(supplier, "AxeGrinder", "Gaming Mouse", 42.00));
session.save(supplier);
tx.commit();
}
this.session = SessionUtil.getSession();
this.tx = this.session.beginTransaction();
}
@AfterMethod
public void closeSession() {
session.createQuery("delete from Product").executeUpdate();
session.createQuery("delete from Supplier").executeUpdate();
if (tx.isActive()) {
tx.commit();
}
if (session.isOpen()) {
session.close();
}
}
}
Listing 9-7chapter09/src/test/java/chapter09/TestBase.java
这里有一个测试,使用了我们在Supplier中的一个命名查询。因为它使用的是已知的数据集,所以它可以查询返回列表的大小,并在此基础上验证查询是否成功。
package chapter09;
import chapter09.model.Supplier;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestNamedQuery extends TestBase{
@Test
public void testNamedQuery() {
Query<Supplier> query = session.getNamedQuery("supplier.findAll");
List<Supplier> suppliers = query.list();
assertEquals(suppliers.size(), 2);
}
}
Listing 9-8chapter09/src/test/java/chapter09/TestNamedQuery.java
当然,我们可以随时创建查询,就像我们在本书中多次展示的那样。
package chapter09;
import chapter09.model.Product;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestSimpleQuery extends TestBase{
@Test
public void testSimpleQuery() {
Query<Product> query = session.createQuery(
"from Product",
Product.class);
query.setComment("This is only a query for product");
List<Product> products = query.list();
assertEquals(products.size(), 5);
}
}
Listing 9-9chapter09/src/test/java/chapter09/TestSimpleQuery.java
createQuery()方法接受一个有效的 HQL 语句(如果需要,还有一个 Java 类型引用,比如Supplier.class)并返回一个org.hibernate.query.Query对象。Query接口提供了将查询结果作为 Java List、Iterator或唯一结果返回的方法。如果你提供一个类型引用,许多操作将使用该类型作为返回值,所以如果你正在寻找一个Supplier实体的列表,你可以使用createQuery("from Supplier s", Supplier.class),并且list()将返回一个List<Supplier>而不是一个List<Object>。其他功能包括命名参数、结果滚动、JDBC 提取大小和 JDBC 超时。您还可以向 Hibernate 创建的 SQL 添加注释,这对于跟踪哪些 HQL 语句对应于哪些 SQL 语句非常有用,如下一节所示。
像所有 SQL 语法一样,您可以用小写或大写(或混合大小写)来编写。但是,您在 HQL 查询中引用的任何 Java 类或属性都必须以正确的大小写指定。例如,当您查询名为 Product 的 Java 类的实例时,HQL 查询"from Product"相当于"FROM Product"。然而,HQL 查询“来自产品”与 HQL 查询"from Product"不同。因为 Java 类名区分大小写,所以 Hibernate 也区分类名的大小写。
记录和注释底层 SQL
Hibernate 可以将 HQL 查询背后的底层 SQL 输出到应用的日志文件中。如果 HQL 查询没有给出您期望的结果,或者如果查询花费的时间比您期望的长,这将特别有用。您可以稍后在数据库的查询分析器中直接运行 Hibernate 针对您的数据库生成的 SQL 来确定问题的原因。这不是一个必须经常使用的特性,但是如果您必须向数据库管理员寻求帮助来调优 Hibernate 应用时,这是非常有用的。
记录 SQL
查看 Hibernate HQL 查询的 SQL 的最简单方法是使用 show_sql 属性在日志中启用 SQL 输出。在您的hibernate.cfg.xml配置文件中设置这个属性为 true, 4 和 Hibernate 将把 SQL 输出到日志中。您不需要启用任何其他日志记录设置,尽管将 Hibernate 的日志记录设置为 debug 也会输出生成的 SQL 语句,以及许多其他文字。
在 Hibernate 中启用 SQL 输出后,您应该重新运行前面的例子(清单 9-9 中的TestSimpleQuery测试)。以下是为产品中的 HQL 语句生成的 SQL 语句:
Hibernate: /* This is only a query for product */ select p1_0.id, p1_1.id, case when p1_1.id is not null then 1 when p1_0.id is not null then 0 end, p1_0.description, p1_0.name, p1_0.price, p1_0.supplier_id, p1_1.version from Product as p1_0 left outer join Software as p1_1 on p1_0.id = p1_1.id
顺便说一下,记住Software类继承自Product,这使得 Hibernate 为这个简单查询生成的 SQL 变得复杂。当我们从简单的Supplier类中选择所有对象时,为 HQL 查询"from Supplier"生成的 SQL 要简单得多:
Hibernate: /* dynamic native SQL query */ select s1_0.id, s1_0.name from Supplier as s1_0
如果您将 Hibernate 类的日志级别提高到 debug 5 ,您将在日志文件中看到 SQL 语句,以及大量关于 Hibernate 如何解析您的 HQL 查询并将其翻译成 SQL 的信息。
注释生成的 SQL
将您的 HQL 语句跟踪到生成的 SQL 可能很困难,因此 Hibernate 在 Query 对象上提供了一个注释工具,允许您将注释应用于特定的查询。Query<R>接口有一个setComment()方法,它接受一个字符串对象作为参数,如下所示:
public Query<R> setComment(String comment)
如果没有一些额外的配置,Hibernate 不会给你的 SQL 语句添加注释,即使你使用了setComment()方法。您还需要在 Hibernate 配置中将 Hibernate 属性use_sql_comments设置为true,如本章前面的清单所示。如果设置了该属性,但没有以编程方式对查询设置注释,Hibernate 将在注释中包含用于生成 SQL 调用的 HQL。我们发现这对于调试 HQL 非常有用。
如果启用了 SQL 日志记录,请使用注释来标识应用日志中的 SQL 输出。例如,如果我们给这个例子添加一个注释,Java 代码看起来会像这样:
String hql = "from Supplier";
Query<Supplier> query = session.createQuery(hql, Supplier.class);
query.setComment("My HQL: " + hql);
List<Supplier> results = query.list();
应用日志中的输出将在 SQL:
Hibernate: /*My HQL: from Supplier*/ select supplier0_.id as id, supplier0_.name as name2_ from Supplier supplier0_
这对于识别日志中的 SQL 非常有用,尤其是当您扫描大量 SQL 时,生成的 SQL 有点难以理解。(运行本章测试中的示例代码是一个很好的例子;这相当于数百行的输出。)
from子句和别名
我们已经在前面的“选择”一节中讨论了 HQL 的 from 子句的基本内容需要注意的最重要的特性是别名。Hibernate 允许您使用 as 子句为查询中的类分配别名。使用别名引用查询中的类。例如,我们之前的简单示例如下:
from Product as p
或以下内容:
from Product as product
您将在应用中看到这两种别名命名约定,尽管它通常用于缩短长查询(因此您将比其他此类形式更经常看到"from Product as p")。as关键字是可选的——您也可以直接在类名后指定别名,如下所示:
from Product product
如果您需要在 HQL 完全限定类名,只需指定包和类名。Hibernate 会在后台处理大部分的事情,所以只有当你的应用中有重名的类时,你才需要这么做。如果必须在 Hibernate 中这样做,请使用如下语法:
from chapter09.model.Product
对于直接处理对象来说,from子句是非常基本和有用的。但是,如果您想处理对象的属性而不将整个对象加载到内存中,您必须使用select子句。
select子句和投影
与from子句相比,select子句提供了对结果集的更多控制。如果您想获得结果集中对象属性的子集——而不是完整的对象本身——使用select子句。例如,我们可以对数据库中的产品运行投影 6 查询,只返回产品名称,而不是将整个对象加载到内存中,如清单 9-10 中所示的类。
package chapter09;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestSimpleProjection extends TestBase {
@Test
public void testSimpleProjection() {
Query<String> query = session.createQuery(
"select p.name from Product p",
String.class);
List<String> suppliers = query.list();
for (String s : suppliers) {
System.out.println(s);
}
assertEquals(suppliers.size(), 5);
}
}
Listing 9-10chapter09/src/test/java/chapter09/TestSimpleProjection.java
这个查询的结果集将包含一个 Java 字符串对象的List。此外,我们可以检索数据库中每个产品的价格和名称,如清单 9-11 所示。
package chapter09;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.Arrays;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestBiggerProjection extends TestBase {
@Test
public void testBiggerProjection() {
Query<Object[]> query = session.createQuery(
"select p.name, p.price from Product p");
List<Object[]> products = query.list();
for (Object[] data : products) {
System.out.println(Arrays.toString(data));
}
assertEquals(products.size(), 5);
}
}
Listing 9-11chapter09/src/test/java/chapter09/TestBiggerProjection.java
当我们在下一章讨论“数据传输对象”或“dto”时,我们将以稍微不同的方式重新审视这个想法。Object[]的一个结果不是天生有用的。
这个结果集包含一个对象数组的List(因此,List<Object[]>)–每个数组代表一组属性(在本例中,是一个名称和价格对)。
如果您只对少数属性感兴趣,这种方法可以减少数据库服务器的网络流量,并节省应用机器上的内存。
对 HQL 使用限制
与 SQL 一样,使用 where 子句选择与查询表达式匹配的结果。HQL 提供了许多不同的表达式,可以用来构建查询。在 HQL 语语法中,有许多可能的表达方式, 7 包括这些:
-
逻辑运算符:
OR、AND、NOT -
相等运算符:
=(表示“相等”)、<>、!=、^=(表示“不相等”) -
比较运算符:
<、>、>=、like、not like、between、not between -
数学运算符:
+、-、*、/ -
串联运算符:
|| -
案例:
Case when <logical expression> then <unary expression> else <unary expression> end -
收藏表情:
some、exists、all、any
此外,还可以在 where 子句中使用以下表达式:
-
HQL 命名的参数,如
:date和:quantity -
JDBC 查询参数:
?(在 HQL 很少使用,应该尽量避免使用命名参数) -
日期和时间 SQL-92 函数运算符:
current_time()、current_date()、current_timestamp() -
SQL 函数(数据库支持):
length()、upper()、lower()、ltrim()、rtrim()等。
使用命名参数
Hibernate 在其 HQL 查询中支持命名参数。这使得编写接受用户输入的查询变得容易——并且您不必防御 SQL 注入攻击。
SQL 注入是一种针对应用的攻击,这些应用通过字符串连接直接从用户输入创建 SQL。例如,如果我们通过 web 应用表单接受用户的姓名,那么像这样构造 SQL(或 HQL)查询将是一种非常糟糕的形式:
String sql = "select p from products where name = '" + name + "'";
恶意用户可以向应用传递一个包含终止引号和分号的名称,后跟另一个 SQL 命令(如 delete from products ),这样用户就可以为所欲为。他们只需要用另一个与 SQL 语句的结束引号相匹配的命令来结束。 8 这是一种非常常见的攻击,尤其是如果恶意用户能够猜出你的数据库结构的细节。
对于每个查询,您可以自己避开用户的输入,但是如果您让 Hibernate 用命名参数管理您的所有输入,那么安全风险会小得多。Hibernate 的命名参数类似于 JDBC 查询参数(?)你可能已经很熟悉了,但是 Hibernate 的参数就没那么混乱了。如果您有一个在多个地方使用相同参数的查询,那么使用 Hibernate 的命名参数也更简单。
使用 JDBC 查询参数时,任何时候添加、更改或删除 SQL 语句的一部分,都需要更新设置其参数的 Java 代码,因为参数是根据它们在语句中出现的顺序进行索引的。Hibernate 允许您为 HQL 查询中的参数提供名称,因此您不必担心在查询中意外地移动参数。
package chapter09;
import chapter09.model.Product;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestNamedParams extends TestBase {
@Test
public void testNamedParams() {
Query<Product> query = session.createQuery(
"from Product where price >= :price",
Product.class);
query.setParameter("price",25.0);
List<Product> products = query.list();
assertEquals(products.size(), 1);
}
}
Listing 9-12chapter09/src/test/java/chapter09/TestNamedParams.java
你甚至可以像这样提供对象引用,如清单 9-13 所示,这里我们看到query.setParameter("supplier", supplier);——这里,我们有一个映射到实际实体的“命名参数”。
package chapter09;
import chapter09.model.Product;
import chapter09.model.Supplier;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals
;
import static org.testng.Assert.assertNotNull;
public class TestNamedEntity extends TestBase {
@Test
public void testNamedEntity() {
Query<Supplier> supplierQuery=session.createQuery(
"from Supplier where name=:name",
Supplier.class);
supplierQuery.setParameter("name", "Supplier 2");
Supplier supplier= supplierQuery.getSingleResult();
assertNotNull(supplier);
Query<Product> query = session.createQuery(
"from Product where supplier = :supplier",
Product.class);
query.setParameter("supplier", supplier);
List<Product> products = query.list();
assertEquals(products.size(), 3);
}
}
Listing 9-13chapter09/src/test/java/chapter09/TestNamedEntity.java
您还可以在 HQL 查询中使用常规的 JDBC 查询参数。我们看不出有什么理由让你想这么做,但是它们确实有效。
在结果集中翻页
对数据库查询的结果集进行分页是一种非常常见的应用模式。例如,通常情况下,您会将分页用于为查询返回大量数据的 web 应用。web 应用将浏览数据库查询结果集,为用户构建合适的页面。如果 web 应用将每个用户的所有数据都加载到内存中,那么应用会非常慢。相反,您可以浏览结果集并检索结果,您将一次显示一个块。
在Query<R>界面上有两种分页方式:setFirstResult()和setMaxResults()。setFirstResult()方法接受一个表示结果集中第一行的整数,从第 0 行开始。您可以用setMaxResults()方法告诉 Hibernate 只检索固定数量的对象。您的 HQL 是不变的——您只需要修改执行查询的 Java 代码。这里有一个测试,展示了分页获取第五到第八个Supplier实体名称的过程。
package chapter09;
import chapter09.model.Supplier;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import java.util.stream.Collectors;
import static org.testng.Assert.assertEquals;
public class TestPagination {
@Test
public void testPagination() {
try (Session session = SessionUtil.getSession()) {
Transaction tx = session.beginTransaction();
session.createQuery("delete from Product").executeUpdate();
session.createQuery("delete from Supplier").executeUpdate();
for (int i = 0; i < 30; i++) {
Supplier supplier = new Supplier();
supplier.setName(String.format("supplier %02d", i));
session.save(supplier);
}
tx.commit();
}
try (Session session = SessionUtil.getSession()) {
Query<String> query = session.createQuery(
"select s.name from Supplier s order by s.name",
String.class);
query.setFirstResult(4);
query.setMaxResults(4);
List<String> suppliers = query.list();
String list = suppliers
.stream()
.collect(Collectors.joining(","));
assertEquals(list,
"supplier 04,supplier 05,supplier 06,supplier 07");
}
}
}
Listing 9-14chapter09/src/test/java/chapter09/TestPagination.java
你可以改变数字,玩分页。如果您打开 SQL 日志记录(就像我们的示例配置那样),您可以看到 Hibernate 使用哪些 SQL 命令进行分页。对于开源的 H2 数据库,Hibernate 使用了offset和fetch first ? rows only。对于其他数据库,Hibernate 使用适当的命令进行分页。如果您的应用在分页方面存在性能问题,这对于调试非常有帮助。
获得独特的结果
正如我们在TestNamedEntity.java源代码中看到的,HQL 的Query<R>接口提供了一个getSingleResult()方法,用于从 HQL 查询中获取一个对象。尽管您的查询可能只产生一个对象,但是如果您将结果限制为第一个结果,您也可以对其他结果集使用getSingleResult()方法。您可以使用上一节讨论的setMaxResults()方法。Query<R>对象上的getSingleResult()方法返回单个对象,如果没有结果,则返回null。如果有多个结果,那么getSingleResult()方法抛出一个NonUniqueResultException。
package chapter09;
import chapter09.model.Product;
import org.hibernate.NonUniqueResultException;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
public class TestSingleResult extends TestBase {
@Test(expectedExceptions = NonUniqueResultException.class)
public void testGetSingleResultBad() {
Query<Product> query = session.createQuery(
"from Product",
Product.class);
Product products = query.getSingleResult();
}
@Test
public void testGetSingleResultGood() {
Query<Product> query = session.createQuery(
"from Product",
Product.class);
query.setMaxResults(1);
Product products = query.getSingleResult();
}
}
Listing 9-15chapter09/src/test/java/chapter09/TestSingleResult.java
这不是好代码!这个例子非常做作,结果是无序的;这个例子仅仅是为了说明以几种不同的方式使用getSingleResult()。
使用 order by 子句对结果进行排序
要对 HQL 查询的结果进行排序,您需要使用order by子句。您可以根据结果集中对象的任何属性对结果进行排序:升序(asc)或降序(desc)。如果需要,可以在查询中对多个属性进行排序。用于排序结果的典型 HQL 查询如下所示:
from Product p where p.price>25.0 order by p.price desc
如果您想按多个属性排序,您只需将附加属性添加到order by子句的末尾,用逗号分隔。例如,您可以先按供应商的名称排序,然后按产品价格排序,如下所示:
from Product p order by p.supplier.name asc, p.price asc
与使用标准查询 API 的等效方法相比,HQL 更易于订购。
关联和加入
在对象关系映射器中,通常以两种方式使用连接。一种是基于连接的标准查询对象——我们在“使用命名参数”一节中看到了这一点。另一种方法是生成“投影”,这是一种数据结构,其唯一的功能是从单个查询中引用自定义对象。
然而,投影可以不仅仅是单个对象的字段子集(这是我们在本章前面的“SELECT”一节中看到的):它可以是 Hibernate 可以表示的任何类型。
这是我们对Product的一组查询。
@NamedQueries({
@NamedQuery(name = "product.searchByPhrase",
query = "from Product p "
+ "where p.name like :text or p.description like :text"),
@NamedQuery(name = "product.findProductAndSupplier",
query = "from Product p, Supplier s where p.supplier=s"),
})
Listing 9-16chapter09/src/main/java/chapter09/model/Product.java
名为product.findProductAndSupplier的查询从表面上看并不特别有用,因为我们可以通过Product中的属性获得产品的供应商。然而,为了便于讨论,让我们假设我们有一个用例,我们希望将产品及其供应商作为单独的字段——可能是因为产品的供应商不能被急切地获取。(同样,这完全是为了一个示例而构建的;在现实世界中,根据我们的数据模型,不需要这样的查询。)
注意,我们返回了两种类型:Product和Supplier,连接在p.supplier上——这意味着“返回每一个产品,对于每一个产品,返回该产品所涉及的供应商。”
这种查询返回的每一行的类型都是Object[]——一个对象数组。
清单 9-17 展示了它的实际效果。
package chapter09;
import chapter09.model.Product;
import chapter09.model.Supplier;
import org.hibernate.query.Query;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestJoinArray extends TestBase {
@Test
public void testJoinArray() {
Query<Object[]> query = session.getNamedQuery(
"product.findProductAndSupplier"
);
List<Object[]> suppliers = query.list();
for (Object[] o : suppliers) {
Assert.assertTrue(o[0] instanceof Product);
Assert.assertTrue(o[1] instanceof Supplier);
}
assertEquals(suppliers.size(), 5);
}
}
Listing 9-17chapter09/src/test/java/chapter09/TestJoinArray.java
当然,使用对象数组并不有趣;事实证明,我们实际上可以在查询中指定对象的类型(并构造它)。 10 这里,我们有一个用作对象的ProductAndSupplier类型——一个元组,一种数据组织——我们的查询指定了如何创建它。注意,我们需要使用完整的包名,因为 Hibernate 不知道它的类型(它不是一个实体),必须准确地告诉它如何创建它。
package chapter09;
import chapter09.model.Product;
import chapter09.model.Supplier;
import org.hibernate.query.Query;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
class ProductAndSupplier {
final Product p;
final Supplier s;
ProductAndSupplier(Product p, Supplier s) {
this.p = p;
this.s = s;
}
@Override
public String toString() {
return "ProductAndSupplier{" +
"p=" + p +
",\n s=" + s +
'}';
}
}
public class TestJoinObject extends TestBase {
@Test
public void testJoinObject() {
Query<ProductAndSupplier> query = session.createQuery(
"select new chapter09.ProductAndSupplier(p,s) " +
"from Product p, Supplier s where p.supplier=s",
ProductAndSupplier.class);
List<ProductAndSupplier> suppliers = query.list();
for (ProductAndSupplier o : suppliers) {
System.out.println(o);
}
assertEquals(suppliers.size(), 5);
}
}
Listing 9-18chapter09/src/test/java/chapter09/TestJoinObject.java
正如我们已经说过的,这个特定的查询示例没有太多意义,来自TestJoinObject的输出证实了这一点;Product输出实际上代表了我们期望的数据。
聚合方法
HQL 支持一系列聚合方法,类似于 SQL。它们在 HQL 中的工作方式与在 SQL 中相同,因此您不必学习任何特定的 Hibernate 术语。不同之处在于,在 HQL 中,聚合方法适用于持久对象的属性。count(...)方法返回给定列名在结果集中出现的次数。您可以使用“count(*)”语法对结果集中的所有对象进行计数,或者使用count(product.name)属性对结果集中的对象数量进行计数。下面是一个使用count(*)方法计算所有产品的示例:
select count(*) from Product product
distinct关键字只计算行集中的唯一值——例如,如果有 100 种产品,但其中 10 种与结果中的另一种产品价格相同,那么select count(distinct product.price) from Product查询将返回 90。在我们的数据库中,以下查询将返回 2,每个供应商一个:
select count(distinct product.supplier.name) from Product product
如果我们删除关键字distinct,它将返回 5,每个产品一个。
所有这些查询都返回列表中的一个Long对象。(换句话说,结果是一个整数值。)您可以在这里使用getSingleResult()方法来获得结果。
通过 HQL 可获得的集合函数包括:
-
avg(property name):房产价值的平均值 -
count(property name or *):属性在结果中出现的次数 -
max(property name):属性值的最大值 -
min(property name):属性值的最小值 -
sum(property name):属性值的总和
如果您有不止一个聚合方法,那么得到的List将包含一个Object数组,其中包含您请求的每个聚合。向 select 子句添加另一个聚合非常简单:
select min(product.price), max(product.price) from Product product
您还可以将这些属性与结果集中的其他投影属性结合起来。
使用 HQL 批量更新和删除
Query<R>接口包含一个名为executeUpdate()的方法,用于执行 HQL UPDATE或DELETE语句。11executeUpdate()方法返回一个包含受更新或删除影响的行数的int,如下所示:
public int executeUpdate() throws HibernateException
基于 SQL UPDATE 语句,HQL 更新看起来就像您期望的那样。更新时不要包含别名;相反,将 set 关键字放在类名之后,如下所示:
String hql = "update Supplier set name = :newName where name = :name";
Query query = session.createQuery(hql);
query.setString("name","SuperCorp");
query.setString("newName","MegaCorp");
int rowCount = query.executeUpdate();
System.out.println("Rows affected: " + rowCount);
//See the results of the update
Query<Supplier> q = session.createQuery("from Supplier", Supplier.class);
List<Supplier> results = q.list();
执行此查询后,任何以前命名为 SuperCorp 的供应商都将被命名为 MegaCorp。您可以在 updates 中使用一个where子句来控制更新哪些行,也可以不使用它来更新所有行。注意,我们打印出了受查询影响的行数。对于这次批量更新,我们还在 HQL 中使用了命名参数。
批量删除的工作方式类似。将delete from子句与您想要删除的类名一起使用。然后使用where子句来缩小表中想要删除的条目的范围。使用executeUpdate()方法也可以对数据库执行删除。
对关系中的对象使用批量删除时要小心。Hibernate 不会知道您删除了数据库中的底层数据,您可能会得到外键完整性错误。
我们围绕 HQL 删除语句的代码基本上是相同的——我们使用命名参数,并打印出受删除影响的行数:
String hql = "delete from Product where name = :name";
Query query = session.createQuery(hql);
query.setString("name","Mouse");
int rowCount = query.executeUpdate();
System.out.println("Rows affected: " + rowCount);
//See the results of the delete
Query<Product> prodQuery = session.createQuery("from Product", Product.class);
List results = prodQuery.list();
在 HQL 中使用批量更新和删除几乎与在 SQL 中一样,所以请记住,这些功能非常强大,如果您在 where 子句中犯了一个错误,就可以删除表中的数据。
使用原生 SQL
尽管您应该尽可能使用 HQL,但是 Hibernate 确实提供了一种直接通过 Hibernate 使用原生 SQL 语句的方法。使用原生 SQL 的一个原因是,您的数据库通过其 SQL 方言支持一些 HQL 不支持的特殊功能。另一个原因是,您可能希望从 Hibernate 应用中调用存储过程。与其他 Java ORM 工具不同,Hibernate 不仅仅提供底层 JDBC 连接的接口,它还提供了一种定义查询使用的实体(或连接)的方法。这使得与其他面向 ORM 的应用的集成变得容易。
您可以修改您的 SQL 语句,使它们与 Hibernate 的 ORM 层一起工作。您确实需要修改您的 SQL 来包含对应于对象或对象属性的 Hibernate 别名。可以用objectname.*指定对象的所有属性,也可以直接用objectname.property指定别名。Hibernate 使用映射将对象属性名转换成底层 SQL 列。这可能不是您所期望的 Hibernate 的确切工作方式,所以请注意,您确实需要修改 SQL 语句以获得对 ORM 的完全支持。在带有子类的类上使用原生 SQL 时,您尤其会遇到问题——请确保您了解如何跨单个表或多个表映射继承,以便从表中选择正确的属性。
Hibernate 的底层原生 SQL 支持是org.hibernate.query.NativeQuery<T>接口,它扩展了org.hibernate.query.Query<T>接口。您的应用将使用会话接口上的createNativeQuery()方法从会话中创建一个本地 SQL 查询(从 QueryProducer 接口继承而来,但这可能比我们需要的更详细)。
public NativeQuery createNativeQuery(String sqlString)
你也可以使用命名的原生的查询。下面是一个使用来自Supplier的命名查询来查找每个Supplier的平均产品价格的示例。
package chapter09;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.Arrays;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestNativeQuery extends TestBase {
@Test
public void testNativeQuery() {
Query query = session.getNamedQuery("supplier.findAverage");
List<Object[]> suppliers = query.list();
for (Object[] o : suppliers) {
System.out.println(Arrays.toString(o));
}
assertEquals(suppliers.size(), 2);
}
@Test
public void testHSQLAggregate() {
Query query = session.getNamedQuery("supplier.averagePrice");
List<Object[]> suppliers = query.list();
for (Object[] o : suppliers) {
System.out.println(Arrays.toString(o));
}
assertEquals(suppliers.size(), 2);
}
}
Listing 9-19chapter09/src/test/java/chapter09/TestNativeQuery.java
在testNativeQuery()中运行的实际查询是:
SELECT p.supplier_id, avg(p.price)
FROM Product p
GROUP BY p.supplier_id
再次注意,这需要底层数据库模式的知识,没有理由不使用 HQL 来获得相同的数据,如测试代码所示,等效的 HQL 为
select p.supplier.id, avg(p.price)
from Product p
GROUP BY p.supplier.id
这里唯一的区别是访问供应商标识符的方式;在第一个查询中,我们使用实际的底层supplier_id列,在后一个查询中,我们遍历图来获得它。在第二个查询中返回实际的Supplier会更有效,但是这里的重点是本地 SQL 执行。
摘要
HQL 是一种强大的面向对象的查询语言,它提供了 SQL 的强大功能,同时利用了 Hibernate 的对象关系映射和固有的缓存。如果要将现有的应用移植到 Hibernate,可以使用 Hibernate 的原生 SQL 工具来对数据库执行 SQL。SQL 功能对于执行特定于给定数据库的 SQL 语句也很有用,在 HQL 中没有对等的 SQL 语句。(它对于执行存储过程也很有用,这个概念依赖于您的特定数据库实现。)
您可以为 Hibernate 打开 SQL 日志记录,Hibernate 将记录它对数据库执行的生成的 SQL。如果向 HQL 查询对象添加注释,Hibernate 将在日志中 SQL 语句旁边显示注释;这有助于在应用中将 SQL 语句追溯到 HQL。
我们的下一章探索从 Hibernate 过滤数据。
*十、过滤搜索结果
您的应用通常只需要处理数据库表中数据的子集。在这些情况下,您可以创建一个 Hibernate 过滤器来使查询忽略不需要的数据。筛选器为您的应用提供了一种将查询结果限制为符合筛选器条件的数据的方法。过滤器并不是一个新概念——使用 SQL 数据库视图或者命名查询也可以达到同样的效果——但是 Hibernate 为它们提供了一个集中的管理系统。
与数据库视图不同, 1 休眠过滤器可以在休眠会话期间启用或禁用。此外,Hibernate 过滤器是参数化的,这在您基于 Hibernate 构建使用安全角色或个性化的应用时特别有用。22
当您有许多带有可归纳选择子句的类似查询时,过滤器特别有用。过滤器允许您使用通用查询,根据需要添加查询条件。
何时使用过滤器
例如,考虑一个使用用户和组的应用。用户具有指示该用户是活动的还是非活动的状态以及一组成员资格;如果您的应用需要根据状态和/或组成员来管理用户,您会看到四个单独的查询(或者通配符查询,这看起来很傻):一个用于所有的状态和组,一个用于状态的子集,一个用于组的子集,一个用于状态和组的子集。通配符查询确实可以工作,但是它会给数据库增加不应该有的负担,特别是如果一组通配符非常常见的话。
如果我们要使用四个不同的查询(“所有用户”、“所有状态为这个的用户”、“所有状态为那个组的用户”和“所有状态为这个的用户和所有状态为那个组的用户”),我们不仅要测试和维护四个查询,还要有一种方法来跟踪我们在任何给定时间应该使用哪个查询。
我们还可以为每次执行使用定制查询(根据需要构建查询,而不是存储一组查询)。这是可行的,但并不完全有效,而且查询数据会污染您的服务。
过滤器允许我们定义限制集。我们可以创建一个过滤器,并在查询数据库时应用它,而不是自定义查询或类似的查询集,这样,即使数据集发生变化,我们的实际查询也不会发生变化。
使用 Hibernate 过滤器的优点是,您可以在应用代码中以编程方式打开或关闭过滤器,并且您的过滤器被定义在一致的位置,以便于维护。过滤器的主要缺点是您不能在运行时创建新的过滤器。相反,应用需要的任何过滤器都需要在适当的 Hibernate 注释或映射文档中指定。虽然这听起来有点限制,但过滤器可以参数化的事实使它们非常灵活。对于我们的用户状态过滤器示例,只需要在映射文档中定义一个过滤器(尽管分为两部分)。该筛选器将指定状态列必须与命名参数匹配。您不需要在 Hibernate 注释或映射文档中定义 status 列的可能值——应用可以在运行时指定这些参数。
尽管用 Hibernate 编写不使用过滤器的应用是完全可能的,但我们发现它们是解决某些类型问题的优秀解决方案——特别是安全性和个性化。
入门指南
在我们走得更远之前,我们应该看一看项目模型。这一次,它非常简单,因为过滤器是 Hibernate 本身的一部分,除了util项目将引入的内容之外,我们没有外部依赖性。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hibernate-6-parent</artifactId>
<groupId>com.autumncode.books.hibernate</groupId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter10</artifactId>
<dependencies>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>util</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
Listing 10-1chapter10/pom.xml
定义和附加过滤器
第一步是创建过滤器定义。过滤器定义类似于过滤器的元数据,包括参数定义;它不是过滤器本身,但它可以作为一个起点。 3 你有了过滤器定义后,你自己创建过滤器;这包含实际的滤波器规格。使用过滤器很简单,只需按名称启用过滤器并填充参数(如果有的话)。
将过滤器定义与过滤器本身分开的原因是什么?这是因为过滤器定义通常是为特定的数据库编写的,因此它们趋向于不可移植。如果过滤器和它们的定义是统一的,那么保持它们的可移植性就更加困难了;有了单独的定义,很容易将过滤器放入单独包含的资源中。
让我们来看看一些过滤器,让他们的用法更清楚。
带注释的过滤器
要使用带注释的过滤器,您需要使用@FilterDef、@ParamDef和@Filter注释。@FilterDef注释定义了过滤器,属于类或包。要定义一个类的过滤器,在@Entity注释旁边添加一个@FilterDef注释。
在您定义了过滤器之后,您可以使用@Filter注释将它们附加到类或集合上。@Filter注释有两个参数:名称和条件。该名称引用了我们之前在注释中描述过的过滤器定义。条件参数是一个 HQL WHERE子句。条件中的参数用冒号表示,类似于 HQL 中的命名参数。必须在过滤器定义中定义参数。以下是过滤器注释的框架示例:
@Entity
@FilterDef(name = "byStatus", parameters = @ParamDef(name = "status", type = "boolean"))
@Filter(name = "byStatus", condition = "status = :status")
public class User {
// other fields removed for brevity's sake
boolean status;
}
在每个类上定义过滤器很简单,但是如果您为多个实体使用一个给定的过滤器,您将会有很多重复。例如,byStatus过滤器可能适用于除了User实体之外的事物。要在包级别定义任何注释,您需要在包中创建一个名为package-info.java的 Java 源文件。package-info.java应该只包含包级注释,然后立即声明包。它并不意味着是一个 Java 类。在配置 Hibernate 时,您还需要告诉 Hibernate 映射包,要么通过AnnotationConfiguration上的addPackage()方法,要么在您的 Hibernate 配置 XML 中:
SessionFactory factory = new MetadataSources(registry)
.addPackage("com.autumncode.entities")
.buildMetadata()
.buildSessionFactory();
在 XML 中,您的映射可能如下所示:
<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:file:./db10</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<mapping class="chapter10.model.User"/>
<mapping package="chapter10.model" />
</session-factory>
</hibernate-configuration>
使用 XML 映射文档的过滤器
对于 XML 映射文档,在一个.hbm.xml文件中使用<filter-def> XML 元素。这些过滤器定义必须包含过滤器的名称以及任何过滤器参数的名称和类型。使用<filter-param> XML 元素指定过滤器参数。下面是一个节选自一个定义了名为latePaymentFilter的过滤器的映射文档,在一个映射一个Account实体的Account.hbm.xml文件中:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="Account" table="ACCOUNTS">
<id name="id" type="int" column="id">
<generator class="native" />
</id>
<property name="dueDate" column="dueDate" type="date" />
<property name="dueAmount" column="dueAmount" type="double" />
</class>
<filter-def name="latePaymentFilter">
<filter-param name="dueDate" type="date"/>
</filter-def>
</hibernate-mapping>
一旦创建了过滤器定义,就需要将过滤器附加到一个类或映射元素的集合上。您可以将单个筛选器附加到多个类或集合。为此,您向每个类和/或集合添加一个<filter> XML 元素。XML 元素有两个属性:名称和条件。该名称引用了过滤器定义(例如,latePaymentFilter)。该条件表示 HQL 中的 WHERE 子句。这里有一个例子:
<class ...
<filter name="latePaymentFilter" condition="paymentDate = :dueDate"/>
</class>
每个<filter> XML 元素必须对应一个<filter-def>元素。
对于大多数应用,更喜欢使用注释而不是 XML。
在应用中使用过滤器
您的应用以编程方式决定为给定的 Hibernate 会话激活或停用哪些过滤器。每个Session可以有一组不同参数值的不同过滤器。默认情况下,会话没有任何活动的筛选器,您必须以编程方式为每个会话显式启用筛选器。会话界面包含几种使用过滤器的方法,如下所示:
public Filter enableFilter(String filterName)
public Filter getEnabledFilter(String filterName)
public void disableFilter(String filterName)
这些是不言自明的——enableFilter(String filterName)方法激活指定的过滤器;disableFilter(String filterName)方法停用过滤器;如果您已经激活了一个已命名的过滤器,getEnabledFilter(String filterName)将检索该过滤器(如果该过滤器未启用,则返回null)。
org.hibernate.Filter接口有六个方法。你不太可能用validate();Hibernate 在处理过滤器时使用这种方法。其他五种方法如下:
public Filter setParameter(String name, Object value)
public Filter setParameterList(String name, Collection values)
public Filter setParameterList(String name, Object[] values)
public String getName()
public FilterDefinition getFilterDefinition()
setParameter()方法是最有用的。您可以用任何 Java 对象替换该参数,尽管其类型应该与您在定义过滤器时为该参数指定的类型相匹配。两个setParameterList()方法对于在过滤器中使用 IN 子句很有用。如果您想使用BETWEEN子句,请使用两个不同名称的过滤器参数。最后,getFilterDefinition()方法允许您检索代表过滤器元数据的FilterDefinition对象(其名称、参数名称和参数类型)。
一旦您在会话上启用了特定的过滤器,您就不必对您的应用做任何其他事情来利用过滤器,正如我们在下面的示例中演示的那样。
一个基本的过滤例子
因为过滤器非常简单,所以一个基本示例允许我们演示大多数过滤器功能,包括激活过滤器和在映射文档中定义过滤器。
我们将创建一个User实体,具有活动状态和组成员资格。我们将定义三个过滤器:一个非常简单的不带参数的过滤器(只是为了演示如何使用),然后是两个参数化的过滤器,我们将在各种组合中应用它们。
我们将定义两个过滤器,以涵盖我们在本章前面提到的四个查询,因为过滤器是附加的——如果我们需要按状态过滤,我们启用状态过滤器,如果我们需要按组过滤,我们可以单独启用过滤器,两个过滤器协同工作。
*我们将坚持使用注释配置,因为我们使用单个数据库(H2),这极大地简化了我们的示例。我们还将恢复使用 Lombok,因为这将通过消除大量样板方法来缩短我们的示例代码。这里是User.java。
package chapter10.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.*;
import javax.persistence.*;
import javax.persistence.Entity;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@Entity
@Data
@NoArgsConstructor
@FilterDefs({
@FilterDef(
name = "byStatus",
parameters = @ParamDef(name = "status", type = "boolean")),
@FilterDef(
name = "byGroup",
parameters = @ParamDef(name = "group", type = "string")),
@FilterDef(
name = "userEndsWith1")
})
@Filters({
@Filter(name = "byStatus", condition = "active = :status"),
@Filter(name = "byGroup",
condition =
":group in (select ug.groups from user_groups ug where ug.user_id = id)"),
@Filter(name = "userEndsWith1", condition = "name like '%1'")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@Column(unique = true)
String name;
boolean active;
@ElementCollection
Set<String> groups;
public User(String name, boolean active) {
this.name = name;
this.active = active;
}
public void addGroups(String... groupSet) {
if (getGroups() == null) {
setGroups(new HashSet<>());
}
getGroups().addAll(Arrays.asList(groupSet));
}
}
Listing 10-2chapter10/src/main/java/chapter10/model/User.java
关于这一点有几件事很突出,特别是关于群体。
首先,组被定义为一个Set,用@ElementCollection标注。这将创建一个表USER_GROUPS,它将包含一个用户 ID 和一个单独的列,在一个以集合命名的列中有不同的组名(因此是“组”而不是“组”)。
如果我们忽略 Hibernate 将为我们创建的外键,表示这种结构的 SQL 可能如下所示:
create table User (
id integer not null,
active boolean not null,
name varchar(255),
primary key (id)
);
create table User_groups (
User_id integer not null,
groups varchar(255)
);
然后,按组选择的筛选器使用子选择来限制返回的用户。这种情况是特定于数据库的,并且使用实际表结构的知识;过滤器会做一些内省,但做得还不够。准备做一些分析,以准确地计算出应该是什么样的过滤条件。
我们还将从我们的util模块中修改SessionUtil类,添加两个方法:doWithSession()和returnFromSession()。这些方法将使我们有机会避免一些管理事务和会话的样板文件。
这是来自util模块的满 SessionUtil.java。
//tag::preamble[]
package com.autumncode.hibernate.util;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.function.Consumer;
import java.util.function.Function;
public class SessionUtil {
private static final SessionUtil instance = new SessionUtil();
private static final String CONFIG_NAME = "/configuration.properties";
private SessionFactory factory;
private Logger logger = LoggerFactory.getLogger(this.getClass());
private SessionUtil() {
initialize();
}
public static Session getSession() {
return getInstance().factory.openSession();
}
public static void forceReload() {
getInstance().initialize();
}
private static SessionUtil getInstance() {
return instance;
}
private void initialize() {
logger.info("reloading factory");
StandardServiceRegistry registry =
new StandardServiceRegistryBuilder()
.configure()
.build();
factory = new MetadataSources(registry)
.buildMetadata()
.buildSessionFactory();
}
//end::preamble[]
public static void doWithSession(Consumer<Session> command) {
try (Session session = getSession()) {
Transaction tx = session.beginTransaction();
command.accept(session);
if (tx.isActive() &&
!tx.getRollbackOnly()) {
tx.commit();
} else {
tx.rollback();
}
}
}
public static <T> T returnFromSession(Function<Session, T> command) {
try (Session session = getSession()) {
Transaction tx = null;
try {
tx = session.beginTransaction();
return command.apply(session);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (tx != null) {
if (tx.isActive() &&
!tx.getRollbackOnly()) {
tx.commit();
} else {
tx.rollback();
}
}
}
}
}
}
Listing 10-3util/src/main/java/com/autumncode/hibernate/util/SessionUtil.java
现在让我们开始创建一些测试。正如我们最近所做的,让我们创建一个chapter10.first.TestBase来构建我们的测试数据。
package chapter10.first;
import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
public class TestBase {
@BeforeMethod
public void setupTest() {
SessionUtil.doWithSession((session) -> {
User user = new User("user1", true);
user.addGroups("group1", "group2");
session.save(user);
user = new User("user2", true);
user.addGroups("group2", "group3");
session.save(user);
user = new User("user3", false);
user.addGroups("group3", "group4");
session.save(user);
user = new User("user4", true);
user.addGroups("group4", "group5");
session.save(user);
});
}
@AfterMethod
public void endTest() {
SessionUtil.doWithSession((session) -> {
// need to manually delete all of the Users since
// HQL delete doesn't cascade over element collections
Query<User> query = session.createQuery("from User", User.class);
for (User user : query.list()) {
session.delete(user);
}
});
}
}
Listing 10-4chapter10/src/test/java/chapter10/first/TestBase.java
我们的第一个测试根本没有使用过滤器。这是我们的基线测试,一路上演示了doWithSession()方法的用法。
package chapter10.first;
import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestNoFilter extends TestBase {
@Test
public void testSimpleQuery() {
SessionUtil.doWithSession((session) -> {
Query<User> query = session.createQuery("from User", User.class);
List<User> users = query.list();
assertEquals(users.size(), 4);
});
}
}
Listing 10-5chapter10/src/test/java/chapter10/first/TestNoFilter.java
那是…不是真的特别有趣,或者有趣。让我们用我们的userEndsWith1过滤器来测试一下,它应用了一个非常简单的条件,接受名字以数字1结尾的任何用户。 4 这将向我们展示如何启用过滤器并验证过滤器的应用。
注意,TestNoFilter和TestSimpleFilter类中的查询是相同的:"from User"。Hibernate 在执行查询时将过滤器作为一个附加的where子句来应用。我们将反复使用这个查询,并使用过滤器来修改结果。
package chapter10.first;
import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestSimpleFilter extends TestBase {
@Test
public void testNoParameterFilter() {
SessionUtil.doWithSession((session) -> {
Query<User> query = session.createQuery("from User", User.class);
session.enableFilter("userEndsWith1");
List<User> users = query.list();
assertEquals(users.size(), 1);
assertEquals(users.get(0).getName(), "user1");
});
}
}
Listing 10-6chapter10/src/test/java/chapter10/first/TestSimpleFilter.java
现在让我们看看如何用过滤器设置参数。这里,我们使用一个 TestNG 数据提供者来传递一个状态和我们期望看到的具有该状态的用户数。
package chapter10.first;
import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestParameterFilter extends TestBase {
@DataProvider
Object[][] statuses() {
return new Object[][]{
{true, 3},
{false, 1}
};
}
@Test(dataProvider = "statuses")
public void testFilter(boolean status, int count) {
SessionUtil.doWithSession((session) -> {
Query<User> query = session.createQuery("from User", User.class);
session
.enableFilter("byStatus")
.setParameter("status", status);
List<User> users = query.list();
assertEquals(users.size(), count);
});
}
}
Listing 10-7chapter10/src/test/java/chapter10/first/TestParameterFilter.java
你也可以组合滤镜。让我们再来一个测试类,但是这次它将有两个单独的测试。第一个将使用我们的byGroup滤镜,第二个将使用两个滤镜—byGroup和byStatus。
再次注意,我们根本没有改变我们的基本查询-from User。如果我们愿意,我们可以。过滤器只需将它们的标准添加到基本查询中。
这是过滤器的实际功率。我们有一个基本的查询—from User——我们可以通过编程来决定对查询应用标准,而不必对查询本身做任何额外的事情。
package chapter10.first;
import chapter10.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class TestMultipleFilters extends TestBase {
@Test
public void testGroupFilter() {
SessionUtil.doWithSession((session) -> {
Query<User> query = session.createQuery("from User", User.class);
session
.enableFilter("byGroup")
.setParameter("group", "group4");
List<User> users = query.list();
assertEquals(users.size(), 2);
session
.enableFilter("byGroup")
.setParameter("group", "group1");
users = (List<User>) query.list();
assertEquals(users.size(), 1);
// should be user 1
assertEquals(users.get(0).getName(), "user1");
});
}
@Test
public void testBothFilters() {
SessionUtil.doWithSession((session) -> {
Query<User> query = session.createQuery("from User", User.class);
session
.enableFilter("byGroup")
.setParameter("group", "group4");
session
.enableFilter("byStatus")
.setParameter("status", Boolean.TRUE);
List<User> users = query.list();
assertEquals(users.size(), 1);
assertEquals(users.get(0).getName(), "user4");
});
}
}
Listing 10-8chapter10/src/test/java/chapter10/first/TestMultipleFilters.java
摘要
过滤器是将一些数据库问题从代码的其余部分中分离出来的有用方法。一组过滤器可以降低应用其余部分中使用的 HQL 查询的复杂性,但会牺牲一些运行时灵活性。与使用视图(必须在数据库级别创建)不同,您的应用可以利用动态过滤器,这些过滤器可以在需要时被激活。
接下来,让我们看看 servlets 的集成,在这里我们将最终开始看到数据传输对象模式的使用。
*十一、整合到网络中
我们已经看到了很多关于如何使用 Hibernate 的内容,包括一系列的示例模型,但是没有一个真正展示了 Hibernate 在“真实世界”中的用法我们所有的运行代码都被嵌入到测试中,这对开发来说并不是一件坏事,但这并不是一个明确如何将 Hibernate 集成到人们可能会使用的应用中的好方法。
测试是一个很好的例子,说明你在“深层代码”、后端服务或嵌入式应用中会看到什么,它们不会直接向最终用户公开 Hibernate 对象——事实上,大多数现代开发人员会建议直接向用户公开 Hibernate 实体是一个坏主意。 1
因此,让我们深入一个 web 应用,展示 Hibernate 实际上是如何集成的。
将 Hibernate 集成到 web 应用中有很多方法,而最佳方法几乎完全取决于部署架构的选择。我们将从非常简单的东西开始,在那里我们控制一切,然后我们将尝试涵盖你在“真实世界”中遇到的更复杂的场景
搭建舞台
Java 中的 Web 应用最初是围绕“servletss”的思想构建的,servlet 基本上是生成对 HTTP 调用的响应的类。这种模式仍然主导着 web 应用的设计,尽管实际的交付方式已经发生了很大的变化。
这个想法是系统将运行一个容器,一个被设计用来管理模块化应用的应用,它使用一个被称为 Java 2 企业版、 2 的规范,旨在为那些应用提供服务以支持特定的功能。例如,像 Tomcat 这样的 web 容器会为 web 模块提供一个 API,这样就可以用一个集中的控制面板来控制它们,并且像 JDBC 连接这样的资源和其他东西可以在运行时提供给这些模块。
这些模块实现功能的方式最初是通过 servletss 的,模块将 servlet 映射到特定的 URL。Web 浏览器(或任何其他应用)将使用这些 URL 和各种 HTTP 方法,如GET和POST来运行 servlets 中的代码。
**那是一个更简单的世界,虽然那种机制起作用(而且,就其本身而言),但它并不是最吸引人的开发过程;如果您想查看客户列表,您的 servlet 必须呈现 HTML,这相当冗长且容易出错。
库的出现使它变得简单了一点(例如,Apache ECS, 3 等等),但是实际上构建一个 web 应用有太多的顾虑;您必须了解 web 模块配置,加上编写 servlet 的功能,再加上将渲染结果编写成 HTML…如果您在显微镜下观察这个过程,这并不是很大的负担,但实际上它给大多数开发人员带来了相当大的负担,而附加值相对较小;大多数网站都没有达到值得努力的流量水平。
自然,社区以多种有益的方式做出了回应(嗯,大部分是有益的)。模型-视图-控制器范式变得非常普遍,在 Struts 和 WebWork 之类的库的帮助下,servlets 只是协调数据处理发生后呈现什么视图。出现了像 JavaServer Pages 这样的渲染引擎,以及像 Velocity 和 Thymeleaf 这样的模板引擎,这甚至还没有开始描述思想和方法的大规模扩散;您可以使用 JVM,用 web 应用编程的书籍单独构建一个库,新的框架会不断涌现,具有各种各样的优势。 4
除此之外,配置也变得更加简单。Spring 框架继承了 Java EE 的重量级特性,并彻底改变了它,强调更容易的部署、配置和开发。更重要的是,由于 Java 在很大程度上是由社区驱动的,Spring 为架构所做的事情被纳入到 Java EE 规范中,就像 Hibernate 对 JPA 所做的一样, 5 就像用 Hibernate 影响 JPA 来为数据库建模 Java 类变得更好一样,使用 Spring 和其他库来影响 Java EE 中以 web 为中心的 API,web 应用开发变得更加容易。
计划
遗憾的是,这本书不是关于 web 应用开发的,而且无论如何也没有合理的方法可以让一本书很好地涵盖 Java 中的 web 开发。尽管如此,在 web 应用中使用 Hibernate 有一些常见的问题,我们可以解决。
我们将使用 Servlets 开发一个 web 应用,其中的某些方面将无法正常工作,因为目的是展示一些问题及其解决方案。本质上,我们不打算设计一个用户界面;这不仅不是你的作者的技能范围,而且是次要问题。我们在这里编写的 servlets 将生成 JSON,它适合使用运行在浏览器中的客户端应用进行渲染;人们可以用 Angular、React 或任何数量的其他 JavaScript 框架来编写这样的应用。
该项目将使用嵌入式 servlet 引擎(Undertow,位于 https://undertow.io/ )进行测试,但也可以部署在任何兼容的 Java EE 容器中——您可以在 Tomcat、WildFly、Open Liberty 或任何其他您喜欢的引擎中随意尝试。同样,该应用的某些部分预计不会正常工作,但这是为了说明问题。
再次声明,这不是关于 web 开发的章节,而是关于在 web 开发中使用 Hibernate 的章节。在本章中,我们不会使用最新、最热门的技术——当涉及到 web 时,我们有目的地降低目标,因为获取我们将要学习的经验并将其应用于更高级的 Web 框架是微不足道的,并且通过关注简单性,我们可以避免陷入大量配置或框架术语中。
应用
我们将设计最普通的应用,博客。
当然,博客是一种“网络日志”,是一种在线日志,这样的应用主宰着网络。如今,大多数博客都托管在大型服务器农场上,如 Medium ( https://medium.com )和 Substack ( https://substack.com ),而大多数个人托管的博客都在软件平台上,如 WordPress ( https://wordpress.org/ )或使用静态站点生成器,如 Jekyll 或 Hugo。 6
我们不会为其中任何一项打造竞争对手。然而,我们将受到作者写有评论的帖子的模式的启发。一个有事业心的读者理论上可以增加安全性(一个需求)和功能性用户界面(另一个需求),然后构建一些有价值的东西。
所以让我们开始吧。
项目模型
首先,我们需要有我们的项目模型。这将包括我们已经看到的许多依赖项,但packaging将是war——因为这将生成一个“web 归档”——它还将包括 Undertow 和 Jackson。
Undertow 是 JBoss 中使用的 servlet 引擎;Jackson ( https://github.com/FasterXML/jackson )是一个数据处理库,我们将用它来读写 JSON。我们还将需要jackson-datatype-jsr310作为依赖项,因为,正如我们很快就会看到的,我们正在使用日期和时间 API 中的一些时间戳类型。 7 我们还包括我们的util库来处理Session的获取和管理,而 Lombok 将为我们节省很多样板文件。
<?xml version="1.0" encoding="UTF-8"?>
<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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>hibernate-6-parent</artifactId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<properties>
<war.name>chapter11</war.name>
<undertow.version>2.2.8.Final</undertow.version>
</properties>
<artifactId>chapter11</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>util</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<version>${undertow.version}</version>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
<version>${undertow.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${war.name}</finalName>
</build>
</project>
Listing 11-1chapter11/pom.xml
数据模型
我们有三个要处理的实体,都在chapter11.model包中:User、Post和Comment。一个User可以有许多Post和Comment条目,一个Post有许多Comment实体。在每一个中,我们都覆盖了 Lombok 的toString(),因为我们想确保不会无意中引用集合。(我们根本不希望 Lombok 输出集合。)
我们不太可能经常使用 toString(),但是它对“目测”结果很有用。如果您在代码中跟随,或者当您开发您自己的模型时,您可能会像您的作者一样,在运行您的测试时使用日志来查看值。

图 11-1
博客的实体关系图
首先,我们来看看User。我们将posts初始化为一个空的ArrayList;我们可能不需要这样做,除非我们发现自己在设计一个过程,通过这个过程,我们在用户被持久化之前向用户添加一个User和帖子,实际上,这样初始化帖子列表本质上是防御性的。(举例来说,这可以防止我们在保存之前无意中引用了帖子列表中的一个null。)
package chapter11.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@Column(unique = true, nullable = false)
String name;
boolean active;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@OrderBy("createDate")
List<Post> posts = new ArrayList<>();
public User(String name, boolean active) {
this.name = name;
this.active = active;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", active=" + active +
'}';
}
}
Listing 11-2chapter11/src/main/java/chapter11/model/User.java
这里的toString()相当普通,但是我们从 Lombok 的toString()中覆盖了它,因为 Lombok 默认包含每个属性。一般来说,这没什么问题,但是如果我们使用 Hibernate 实体——也就是从 Hibernate 加载的对象——那么我们最终会冒着在一个Session关闭后需要初始化惰性集合的风险,这意味着访问posts可能会导致我们获得一个org.hibernate.LazyInitializationException。
要求会话在任何时候都在范围内——或者,好吧,当我们应该完成对Session的访问时——是本章的主要关注点之一。
Post是我们拥有的三个实体中最有趣的一个,因为它使用了 Hibernate 过滤器定义。我们先来看一下,然后在一些有趣的方面游走。
package chapter11.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.*;
import javax.persistence.*;
import javax.persistence.OrderBy;
import java.time.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Data
@NoArgsConstructor
@FilterDefs({
@FilterDef(
name = "byTerm",
parameters = @ParamDef(name = "term", type = "string")),
@FilterDef(
name = "byName",
parameters = @ParamDef(name = "name", type = "string")
)
})
@Filters({
@Filter(name = "byTerm",
condition = "title like :term"),
@Filter(name = "byName",
condition = "user_id = (select u.id from User as u where u.name=:name)"),
})
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@Column(nullable = false)
String title;
@Column(nullable = false)
@Lob
String content;
@ManyToOne
User user;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
@OrderBy("createDate")
List<Comment> comments = new ArrayList<>();
@Temporal(TemporalType.TIMESTAMP)
@Column(nullable = false)
LocalDateTime createDate;
@Override
public String toString() {
return "Post{" +
"id=" + id +
", title='" + title + '\'' +
", content='" + content + '\'' +
", user=" + user +
", createDate=" + createDate +
'}';
}
}
Listing 11-3chapter11/src/main/java/chapter11/model/Post.java
我们实际上有两个过滤器:byTerm和byName。
第一个是byTerm,是一个相当简单的单词搜索过滤器,更多的是作为一个占位符,而不是一个实际的搜索工具;它包括任何标题包含通配符的Post,即过滤器名称中的“term”。
byName滤镜更有趣一点。它实际上包括任何其user_id字段与具有给定名称的User相匹配的Post——因此,它通过用户名为任何Post提供过滤器。之所以这样写是因为它是一个过滤器而不是一个查询;编写一个 HQL 查询来实现相同的目标可能会更容易,但是当我们开始使用过滤器时,过滤器的原因就足够清楚了。
JPA 有一个标准查询 API,它提供了与 Hibernate 过滤器相似的特性。然而,尽管过滤器可能变得冗长,但它们仍然比标准查询冗长得多,并且它们需要较少的设置工作。
我们的最后一个实体是Comment,在看过Post和User之后,它看起来有点普通和无聊。
package chapter11.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Data
@NoArgsConstructor
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@Column(nullable = false)
@Lob
String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
Post post;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
User user;
@Temporal(TemporalType.TIMESTAMP)
@Column(nullable = false)
LocalDateTime createDate;
@Override
public String toString() {
return "Comment{" +
"id=" + id +
", content='" + content + '\'' +
", createDate=" + createDate +
'}';
}
}
Listing 11-4chapter11/src/main/java/chapter11/model/Comment.java
如果没有 Hibernate 配置,我们就不能拥有 Hibernate 应用,所以它在这里;注意,除了实际的特定映射之外,它与所有其他的都非常相似。它也位于src/main/resources而不是src/test/resources中,主要是因为我们希望我们的可部署工件是完整的和可部署的;如果 Hibernate 配置位于test树中,它就不是可部署工件的一部分。
<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:./db11</property>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<property name="use_sql_comments">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create-drop</property>
<mapping class="chapter11.model.User"/>
<mapping class="chapter11.model.Post"/>
<mapping class="chapter11.model.Comment"/>
</session-factory>
</hibernate-configuration>
Listing 11-5chapter11/src/main/resources/hibernate.cfg.xml
构建我们的第一个 Servlet 测试
接下来,我们要看看可能是整章中最长的类,我们的TestBase servlet。
实际上,有很多方法可以解决为 web 应用编写代码的问题。最优选的方法是编写映射到我们的用户故事的服务——像“添加用户”或“添加帖子”这样的功能——然后使用 servlets 调用这些服务。
这个模型实际上工作得非常好,但是它确实设法自然地将您与在分布式应用中使用 Hibernate 的一些架构问题隔离开来。当您像这样分离关注点时——“添加用户”在一个完全独立的类中——您有一个自然的事务边界,并且很容易意外地正确和干净地做事情。我们将在后续章节中使用该模型,但我们需要理解为什么我们将做出的一些选择存在,所以现在我们要做一些稍微次优的事情。
但是,结果是,我们的测试不能只关注服务——我们需要建立一个实际的 web 服务器,使用实时的 servlets。这不是一个完全的集成测试, 8 但是它实际上使用一个真正的 HTTP 客户端(Java 11 中包含的那个)并发出真正的 HTTP 请求;如果您愿意,您可以手动做与测试完全相同的事情。
*那么,我们的TestBase有很多角色:它需要启动(和停止)逆流,以及注册一系列 servlets。它还需要提供一种简单的方法来发出 HTTP 请求,并构建一种简单的方法来将 JSON 映射到可导航的数据结构中。
我们不需要使它完美,只要对我们的目的足够好就行了。通过各种方式让TestBase变得更好并不需要付出太多的努力,但它已经够长了。
TestBase中的大部分复杂性存在于populateServlets()方法中,它制造了一个DeploymentInfo。它加载一个名为servlets.json的 JSON 文件,其中包含一个 JSON 字典;每个条目都是一个 servlet,它有关于每个条目的数据,比如 Servlet 的实现类和 URL,以及它可能使用的任何初始化参数。
这里是TestBase.java类,我们将在其后立即包含servlets.json,这样您就可以看到它的结构。 9
package chapter11.servlets;
import chapter11.model.Comment;
import chapter11.model.Post;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.server.handlers.PathHandler;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import org.hibernate.query.Query;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
public class TestBase {
Undertow server;
TypeReference<Map<String, Object>> mapOfMaps =
new TypeReference<>() {
};
protected ObjectMapper mapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
{
mapper.registerModule(new JavaTimeModule());
}
@BeforeClass
void start() throws ServletException, IOException {
DeploymentInfo servletBuilder = Servlets.deployment()
.setClassLoader(TestBase.class.getClassLoader())
.setContextPath("/myapp")
.setDeploymentName("test.war");
populateServlets(servletBuilder);
DeploymentManager manager = Servlets
.defaultContainer()
.addDeployment(servletBuilder);
manager.deploy();
PathHandler path = Handlers.path(Handlers.redirect("/myapp"))
.addPrefixPath("/myapp", manager.start());
server = Undertow.builder()
.addHttpListener(8080, "localhost")
.setHandler(path)
.build();
server.start();
}
private void populateServlets(DeploymentInfo servletBuilder)
throws IOException {
Map<String, Object> servlets = mapper
.readValue(
this
.getClass()
.getResourceAsStream("/servlets.json"
), mapOfMaps);
servlets.entrySet().forEach(entry -> {
Map<String, Object> data =
(Map<String, Object>) entry.getValue();
try {
var servlet = Servlets.servlet(
entry.getKey(),
(Class<? extends Servlet>) Class.forName(
data.get("class").toString()
));
if (data.containsKey("initParams")) {
Map<String, Object> params =
(Map<String, Object>) data.get("initParams");
params.entrySet().forEach(param -> {
servlet.addInitParam(
param.getKey(),
param.getValue().toString()
);
});
}
servlet.addMapping(data.get("mapping").toString());
servletBuilder.addServlets(servlet);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
});
}
@AfterClass
void stop() {
server.stop();
}
@BeforeMethod
void clearAll() {
SessionUtil.doWithSession(session -> {
Query<Comment> commentQuery =
session.createQuery("from Comment", Comment.class);
for (var obj : commentQuery.list()) {
session.delete(obj);
}
Query<Post> postQuery =
session.createQuery("from Post", Post.class);
for (Post post : postQuery.list()) {
session.delete(post);
}
Query<User> query =
session.createQuery("from User", User.class);
for (User user : query.list()) {
session.delete(user);
}
});
}
protected HttpResponse<String> issueRequest(String path)
throws IOException, InterruptedException {
HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/myapp/" + path))
.timeout(Duration.ofSeconds(3))
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
return response;
}
}
Listing 11-6chapter11/src/test/java/chapter11/servlets/TestBase.java
这是一个相当大的类,与我们到目前为止所拥有的许多源文件相比,但是它并不特别复杂。大部分都与设置我们的 servlets 有关,但是它也清理了数据库,并且有一个方便的方法来发出 HTTP 请求。
mapper引用为我们提供了一种简单的方法来正确测试带有日期的序列化数据。
start()方法在每个测试类之前运行,启动 Undertow servlet 引擎,还有populateServlets(),它使用一个 JSON 文件构建一个入口点列表,我们接下来会看到。它使用原始的 JSON 结构,这并不理想;通常,我们会构建一个对象模型,并使用 that ,但是我们试图选择更少的类。
stop()方法在每个测试类测试后运行,并关闭 Undertow 服务器。
clearAll()方法清理所有的数据,因为它被标记为@BeforeMethod,所以它在每次测试之前运行;从数据的角度来看,每个测试都是从一张白纸开始的。
最后,issueRequest()提供了一种简单的方法来构建用于测试的 HTTP 请求。这是荒谬的简单;它只处理请求路径,不进行参数化,并且只处理 HTTP GET请求。然而,它确实去除了许多直接发出请求的样板文件。
那么,为什么连都有呢?毕竟,我们不需要使用 HTTP 作为传输协议;我们可以拥有执行 servlet 提供的动作的类,并直接调用这些类。服务器或协议不需要或。尽管如此,协议障碍是我们试图证明的东西,所以所有的设置都是为了说明在某种“真实世界”情况下的架构边界,如 HTTP 的使用,即使在“真实世界”中没有人会合理地手动进行任何操作。
这里是本章使用的实际的servlets.json文件,包括了每个 servlet。(如果您在阅读时手动构建了本章的代码库,那么您会想要参考这个清单,而不是大量使用它,直到您实现了所有的 servlets。)
{
"HelloServlet": {
"class": "chapter11.servlets.HelloWorld",
"initParams": {
"message": "Hello World"
},
"mapping": "/hello"
},
"BadAddUserServlet": {
"class": "chapter11.servlets.BadAddUserServlet",
"mapping": "/badadduser"
},
"AddUserServlet": {
"class": "chapter11.servlets.AddUserServlet",
"mapping": "/adduser"
},
"SimpleGetPostsServlet": {
"class": "chapter11.servlets.SimpleGetPostsServlet",
"mapping": "/simplegetposts"
},
"AddPostServlet": {
"class": "chapter11.servlets.AddPostServlet",
"mapping": "/addpost"
},
"GetPostsServlet": {
"class": "chapter11.servlets.GetPostsServlet",
"mapping": "/getposts"
},
"GetPostServlet": {
"class": "chapter11.servlets.GetPostServlet",
"mapping": "/getpost"
},
"AddCommentServlet": {
"class": "chapter11.servlets.AddCommentServlet",
"mapping": "/addcomment"
}
}
Listing 11-7chapter11/src/test/resources/servlets.json
我们的 servlets 被设计成发出 JSON,具体来说,这听起来是创建一个ServletBase、一个抽象的HttpServlet实现的理想理由,它提供了一种将Object写成 JSON 的便捷方式。
package chapter11.servlets;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
abstract class ServletBase extends HttpServlet {
protected ObjectMapper mapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
{
mapper.registerModule(new JavaTimeModule());
}
/* simple validation of parameters */
protected Map<String, String> getValidatedParameters(
HttpServletRequest req,
String... fields
) {
Map<String, String> map = new HashMap<>();
List<String> badFields = new ArrayList<>();
for (String fieldName : fields) {
String value = req.getParameter(fieldName);
if (value == null || value.isEmpty()) {
badFields.add(fieldName);
} else {
map.put(fieldName, value);
}
}
if (badFields.size() > 0) {
throw new RuntimeException(
"bad fields provided: " + badFields
);
}
return map;
}
/* write out a valid response */
protected void write(
HttpServletResponse r,
int code,
Object entity
) throws IOException {
r.setContentType("application/json");
r.setStatus(code);
r.getWriter().write(mapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(entity)
);
}
/* write out an exception */
protected final void writeError(
HttpServletResponse resp,
Throwable throwable
) throws IOException {
write(resp,
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
Map.of("error", throwable.getMessage())
);
}
}
Listing 11-8chapter11/src/main/java/chapter11/servlets/ServletBase.java
我们使用setSerializationInclusion(JsonInclude.Include.NON_NULL)是因为我们希望在序列化数据时忽略包含null的字段。这个重要吗?嗯,不在这里;毕竟,我们没有真正的用户界面,而且我们拥有的总计字段的数量如此之少,以至于包含一些对空数据的引用在任何情况下都无关紧要。尽管如此,这允许一个潜在的用户界面检查字段的存在,而不必检查字段是否为空。这只是防御性编程。
*我们还禁用了WRITE_DATES_AS_TIMESTAMPS特性,因为我们使用了LocalDateTime并且我们真的希望它以人类可读的形式编写。不过,要做到这一点,我们需要在 Jackson 中注册JavaTimeModule(),这是用初始化器块完成的。
我们还有一个getValidatedParameters()方法,该方法返回字段名及其值的一个Map<String, String>——如果一个字段没有被提供(并且是必需的),则抛出一个RuntimeException。这是非常原始的做法,有更好的方法。
接下来,我们有write()和writeError()方法,它们为我们的输出以标准形式格式化输出。
ServletBase是功能正常但不太好的。您可能不想在这个层次上编写 servlets,但是有时理解底层技术在做什么是很重要的。
我们在servlets.json中看到的第一个 servlet 是一个“Hello,World”servlet,名为“HelloServlet”,在一个名为HelloWorld的类中实现。这是一个简单的 servlet,它验证我们的 Undertow 实例是否工作,并使用我们的ServletBase写入输出。
package chapter11.servlets;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class HelloWorld extends ServletBase {
@Override
protected void doGet(
HttpServletRequest request,
HttpServletResponse response)
throws IOException {
Map<String, String> data=Map.of(
"response", this.getInitParameter("message")
);
write(response, HttpServletResponse.SC_OK, data);
}
}
Listing 11-9chapter11/src/main/java/chapter11/servlets/HelloWorld.java
这里有两件有趣的事情。一个是使用一个“init 参数”来获取响应的值(在servlets.json的<initParams>节点中提供),另一个是我们首先构建一个Map<String, String>来生成响应;JSON 是一个数据结构,我们不能用 JSON 写一个简单的字符串作为输出。当然,我们可以直接编写输出,但是我们正在尝试实际测试我们的应用将使用的管道。
我们现在需要测试我们的HelloWorld servlet。有了TestBase,我们可以创建一个HelloWorldTest,看起来像清单 11-10 中所示的那个。
package chapter11.servlets;
import org.testng.annotations.Test;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.Map;
import static org.testng.Assert.assertEquals;
public class HelloWorldTest extends TestBase {
@Test
public void testHelloWorld()
throws IOException, InterruptedException {
HttpResponse<String> response =
issueRequest("hello");
Map<String, Object> data =
mapper.readValue(response.body(), mapOfMaps);
assertEquals(
response.statusCode(),
HttpServletResponse.SC_OK
);
assertEquals(
data.get("response"),
"Hello World"
);
}
}
Listing 11-10chapter11/src/test/java/chapter11/servlets/HelloWorldTest.java
我们的HelloWorldTest真的非常简单:在通过TestBase初始化之后,它发出一个请求(通过晦涩命名的issueRequest()方法,保存在TestBase中),返回一个String——一个应该包含 JSON 的字符串。
然后,我们通过保存在TestBase中的mapper实例对其进行解析,并验证响应代码是否为SC_OK(即 200,表示请求成功的 HTTP 代码),以及 JSON map 是否有一个名为response的属性,其值为Hello World。
我们可以创建一个包含response字段的对象,并将我们的 JSON 映射到该对象,而不是使用我们在这里演示的“映射的映射”方法。更重要的是,这可能是做这件事的“正确方法”,我们将在下一个例子中看到这样的例子;我们在这里避免了它,因为它只是另一件需要解释的事情。我们已经引入了足够多的新概念。如果我们把它们分开一些会更好。
这种发出请求并检查响应的模式将在我们的测试中重复出现。
我们的第一个(错误的)Servlet:添加用户
让我们创建一个 servlet 来向系统添加一个新用户。在其中,我们将接受一个用户名,并检查该用户是否已经存在;如果用户不存在,我们将创建用户,然后将用户作为 JSON 返回。
这个过程表面上看起来是正确的。然而,这个代码是错误的。我们很快就会看到原因。
package chapter11.servlets;
import chapter11.dto.UserDTO;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;
import javax.persistence.NoResultException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class BadAddUserServlet extends ServletBase {
@Override
protected void doGet(
HttpServletRequest req,
HttpServletResponse resp
)
throws ServletException, IOException {
try {
Map<String, String> input =
getValidatedParameters(req, "userName");
User user = SessionUtil.returnFromSession(
session -> createUser(session, input.get("userName"))
);
write(resp,
HttpServletResponse.SC_OK,
user);
} catch (Exception e) {
writeError(resp, e);
}
}
private User createUser(Session session, String userName) {
User entity;
try {
Query<User> query = session.createQuery(
"from User u where u.name=:name",
User.class);
query.setParameter("name", userName);
entity = query.getSingleResult();
} catch (NoResultException nre) {
entity = new User(userName, true);
session.save(entity);
}
return entity;
}
}
Listing 11-11chapter11/src/main/java/chapter11/servlets/BadAddUserServlet.java
我们的 servlet 功能的入口点是doGet(),它映射到用于调用 Servlet 的 HTTP 方法。(因此,它响应GET请求,出于我们的目的,其他方法将被忽略。)它使用getValidatedParameters()来验证参数。
然后,它创建一个User引用,并通过调用createUser()来调用SessionUtil.returnFromSession()。在那之后,除了异常处理之外,实际上没什么可做的了:它将User引用写入输出。
然而,用来获取User的机制是这个 servlet 失败的地方。
我们的设计是返回映射到周期userName的User。如果User已经存在,它从数据库加载用户并返回那个。(从整体来看,这是否明智还存在争议;对于一个“真正的应用”,您不会像这样返回一个现有的用户,以防有人发送“添加用户”功能来查找现有的用户名。)
错误很简单:我们使用的 JSON 库 Jackson 将遍历User中的每个属性,并在 JSON 中为它创建一个表示。然而,如果用户是由createUser()创建的,那么User的posts引用被设置为一个简单的ArrayList——但是如果它是从数据库加载的,那么posts引用实际上是一个代理值,映射User的尝试将导致 Hibernate 尝试从数据库加载Post列表。
然而,映射发生在λ存在的之后的:Session不再活动,因此我们将得到一个LazyInitializationError。
这是我们的测试类,它实际上练习了 Servlet 中的大部分代码。
package chapter11.servlets;
import org.testng.annotations.Test;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import static org.testng.Assert.assertEquals;
public class BadAddUserServletTest extends TestBase{
String getServletName() {
return "badadduser";
}
@Test
void emptyUserNameProvided()
throws IOException, InterruptedException {
HttpResponse<String> response =
issueRequest(getServletName()+"?userName=");
System.out.println(response.body());
assertEquals(
response.statusCode(),
HttpServletResponse.SC_INTERNAL_SERVER_ERROR
);
}
@Test
void noUserNameProvided()
throws IOException, InterruptedException {
HttpResponse<String> response =
issueRequest(getServletName());
System.out.println(response.body());
assertEquals(
response.statusCode(),
HttpServletResponse.SC_INTERNAL_SERVER_ERROR
);
}
@Test
void runAddUser()
throws IOException, InterruptedException {
HttpResponse<String> response =
issueRequest(getServletName()+"?userName=ts");
System.out.println(response.body());
assertEquals(
response.statusCode(),
HttpServletResponse.SC_OK
);
response = issueRequest("badadduser?userName=ts");
assertEquals(
response.statusCode(),
HttpServletResponse.SC_INTERNAL_SERVER_ERROR
);
}
}
Listing 11-12chapter11/src/test/java/chapter11/servlets/BadAddUserServletTest.java
首先,它有一个emptyUserNameProvided——这将触发一个验证错误,就像根本不提供userName参数一样,如noUserNameProvided测试所示。(它使用getServletName()是因为我们想在另一个测试中重用其中的一些方法,除了另一个 servlet。)
然后我们进入runAddUser测试,该测试调用我们的 servlet 两次:一次是首先添加用户,然后取回同一个用户……除了第二次调用失败并出错,这是因为惰性初始化。我们不测试实际的异常;我们可以,因为我们控制着 servlet 的源代码,但是在实践中,人们通常不会向最终用户公开原始的应用异常,这不会是一个“可测试的结果”(您可以将这样的异常转换成对用户来说更可展示的东西,并测试和内容。)
AddUserServlet,已更正
理论上,我们有几种方法可以解决BadAddUserServlet的问题。一种是用@com.fasterxml.jackson.annotation.JsonIgnore标记posts元素,这告诉 Jackson 忽略该属性。然而,可能有些情况下我们确实想要整个用户的历史;将字段标记为“忽略”感觉有点宽泛。
一个更好的解决方案可能是 10 使用一个叫做“数据传输对象”的东西,这个类的目的纯粹是为了在架构边界之间进行传输。
例如,在这种情况下,我们不关心posts——事实上,我们也不想要它们,因为加载一个活跃用户的帖子集可能需要相当长的时间。因此,我们的数据传输对象,或 DTO,将只有来自User : id、name和active的我们真正关心的字段。清单 11-13 展示了它可能的样子。
package chapter11.dto;
import lombok.Data;
@Data
public class UserDTO {
int id;
String name;
boolean active;
public UserDTO() {
}
public UserDTO(
int id,
String name,
boolean active
) {
this.id = id;
this.name = name;
this.active = active;
}
}
Listing 11-13chapter11/src/main/java/chapter11/dto/UserDTO.java
我们给它一个惟一的名字UserDTO,因为当 DTO 和实体在同一个源文件中时,我们可能不得不使用完全限定的类名(即chapter11.dto.User)。
那么我们将如何利用它呢?好吧,这里有一个工作的AddUserServlet,写的结构和BadAddUserServlet几乎一模一样,除了用UserDTO来回传递数据,而不是用User。
package chapter11.servlets;
import chapter11.dto.UserDTO;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;
import javax.persistence.NoResultException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class AddUserServlet extends ServletBase {
@Override
protected void doGet(
HttpServletRequest req,
HttpServletResponse resp
)
throws ServletException, IOException {
try {
Map<String, String> input =
getValidatedParameters(req, "userName");
UserDTO user = SessionUtil.returnFromSession(
session -> createUser(session, input.get("userName"))
);
write(resp,
HttpServletResponse.SC_OK,
user);
} catch (Exception e) {
writeError(resp, e);
}
}
protected UserDTO createUser(Session session, String userName) {
User entity;
try {
Query<User> query = session.createQuery(
"from User u where u.name=:name",
User.class);
query.setParameter("name", userName);
entity = query.getSingleResult();
} catch (NoResultException nre) {
entity = new User(userName, true);
session.save(entity);
}
UserDTO dto = new UserDTO();
dto.setId(entity.getId());
dto.setName(entity.getName());
dto.setActive(entity.isActive());
return dto;
}
}
Listing 11-14chapter11/src/main/java/chapter11/servlets/AddUserServlet.java
我们可以使用不同类型的查询直接构建 DTO,而不是通过从User到UserDTO的映射。我们仍然需要能够构造一个User实体,这意味着我们仍然需要手动映射到一个UserDTO实例。在本章的剩余部分,我们不会遵循这种模式,但是这里有一个基于AddUserServlet的示例实现,覆盖了createUser()来展示这个过程。
package chapter11.servlets;
import chapter11.dto.UserDTO;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;
import javax.persistence.NoResultException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class AddUserServletDTO extends AddUserServlet {
protected UserDTO createUser(Session session, String userName) {
UserDTO dto;
try {
Query<UserDTO> query = session.createQuery(
"select new chapter11.dto.UserDTO(u.id, u.name,u.active) "
+"from User u where u.name=:name",
UserDTO.class);
query.setParameter("name", userName);
dto = query.getSingleResult();
} catch (NoResultException nre) {
User u = new User(userName, true);
session.save(u);
dto = new UserDTO(u.getId(), u.getName(), u.isActive());
}
return dto;
}
}
Listing 11-15chapter11/src/main/java/chapter11/servlets/AddUserServletDTO.java
如果您想使用这个类而不是AddUserServlet,您可以修改servlets.json来简单地引用这个类名。
自然要问是否管用。我们将继承来自BadAddUserServletTest的几乎所有东西,因为我们想要验证使用不正确的参数仍然会导致错误,但是我们将覆盖runAddUser来实际验证它获得了 HTTP 响应代码 200——这意味着成功的请求——而不是预期的错误。清单 11-16 显示了来源。
package chapter11.servlets;
import org.testng.annotations.Test;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.Map;
import static org.testng.Assert.assertEquals;
public class AddUserServletTest
extends BadAddUserServletTest {
String getServletName() {
return "adduser";
}
@Override
@Test
void runAddUser()
throws IOException, InterruptedException {
HttpResponse<String> response =
issueRequest("adduser?userName=jbo");
Map<String, Object> data =
mapper.readValue(response.body(), mapOfMaps);
assertEquals(
response.statusCode(),
HttpServletResponse.SC_OK
);
response = SimpleGetPostsService.getSimplePosts();
assertEquals(
response.statusCode(),
HttpServletResponse.SC_OK
);
}
}
Listing 11-16chapter11/src/test/java/chapter11/servlets/AddUserServletTest.java
现在我们可以说我们可以正确地添加用户了,使用我们对“添加用户”过程的定义:不管数据库是否被修改,它都会正确地返回一个User表示。
dto 闪耀的地方
展示的经验法则很简单,它强化了我们在本书第三章的早期内容。从 Hibernate 加载的实体实际上是由代理的对象,只要它们由Session管理,它们的数据就会被填充。一旦实体与Session分离,它们的状态就固定了;如果他们的数据没有从数据库中加载,那么他们的数据就不能从数据库中加载,直到他们以某种方式再次连接到Session。
如果你没有从本章学到其他东西,那就从上一段吸取教训。这一章中有很多非常有趣的代码,尽管其中大部分对“真正的应用”来说用处有限,但是前面的段落是主要的要点。其他的都是证明和示范。
那么,这两种方法是在从实体被加载时返回之前完全加载实体和创建实体的分离版本,这就是我们对UserDTO所做的。我们也可以用User的副本来完成:
User user=Session.load(userId, User.class);
// we never use clone() in Java, right?
User copy=new User();
copy.setId(user.getId());
copy.setName(user.getName());
return copy;
通过创建一个新的User而不是让它成为一个托管对象,这可以像使用UserDTO一样容易地避免惰性初始化问题。
*如果需要维护的类更少,为什么不使用创建实体的非托管实例的策略呢?没有一个真正好的、可靠的答案,尽管有一些促成因素。对我来说,这主要归结于目的;我希望班级有一个特定的角色。应该使用像User这样的实体类将数据映射到数据库或从数据库映射数据;使用它在 servlet 和富客户机之间传输数据是要求它做双重工作,在这种情况下,很容易忘记给定的类扮演什么角色;这个User实例是托管的,还是被用来序列化 JSON?
如果我使用一个UserDTO,那么这不是一个我必须问的问题;如果我正在序列化为 JSON,那就是一个UserDTO,句号。如果我从数据库中读取,它是一个User实体,我知道如果我像这样跨越架构边界,我需要来回转换。此外,我可以创建任意多的数据传输类型——如果我想要一个包含用户帖子的 DTO,我可以创建一个UserPostsDTO,或者如果我想要包含他们的评论,我可以创建一个UserCommentsDTO,并精确地控制数据。 11
完善应用
那我们该怎么办?应用需求的核心方面可能类似于
-
创建用户(我们已经展示过了)
-
创建帖子
-
给帖子添加评论
-
按日期获取帖子
-
按用户获取帖子
-
通过关键字获取帖子
大多数都很简单,尽管最后三个很有趣,因为它们将使用我们在Post实体中设置的过滤器。甚至很有可能将大量功能抽象成流程——毕竟,对于每一个流程,我们都要验证输入(以一种相当简单的方式),然后调用一个方法来生成输出。
实际上,对于读者来说,进行这种重构可能是一项值得做的工作,但是如果不进行重构,我们的清单将会很长,所以我们将直接进入。先来看看我们是怎么创建帖子的。
创建帖子
根据我们的模型,为我们创建一个帖子包括帖子的内容和标题以及有效的用户参考。实现这一点的实际代码相当简单——测试将带来一些非常严重的影响。
例如,考虑获取有效用户引用的过程。我们的AddUserServlet有这样做的代码,它不是很长——19 行代码,带有打印的硬包装和到UserDTO的冗长转换。
在正常情况下,我们会创建一个服务类来保存我们的功能,所以我们会调用一个getOrCreateUser()方法,不管有没有Session——例如,我们在第三章中已经看到了这种模式。这给了我们容易嵌入的功能,而不必担心会话分界12——对象离开托管状态的地方。
事实上,服务对象允许我们几乎不用考虑就能维护分界障碍,这是使用它们的一个很好的理由。然而,在这一章中,会话的开始和结束是我们试图研究的核心课程——我们正在研究障碍以及如何考虑它们——因此,为了了解解决方案为什么以及如何工作,我们避免使用明显的解决方案。
所以,首先,让我们开始为我们的测试构建工具,这样我们就可以构建可组合的服务调用来对我们隐藏issueRequest()。我们仍然需要HttpResponse对象,所以我们可以测试响应的主体和响应代码,但至少我们可以隐藏许多进行服务调用本身的常见方面。
我们将从一个叫做BaseService的类开始。我们将把这些设计成静态类,因为它们完全没有状态要管理,虽然这是实际实现的反模式,但该反模式仅限于本章和我们的测试结构。我们并没有将它们设计成可以替代实际的服务呼叫。 13
package chapter11.servlets;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.Charset;
import java.time.Duration;
public class BaseService {
static String encode(String value) {
return URLEncoder.encode(
value,
Charset.defaultCharset()
);
}
static HttpResponse<String> issueRequest(String path)
throws IOException, InterruptedException {
HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/myapp/" + path))
.timeout(Duration.ofSeconds(3))
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
return response;
}
}
Listing 11-17chapter11/src/test/java/chapter11/servlets/BaseService.java
正如您所看到的,这个类简单地保存了一个简单的issueRequest()以及一个对 HTTP 参数进行编码的方法。通过这样做,我们可以节省一些代码,但不是很多——毕竟,我们已经在TestBase中有了一个issueRequest()的副本,正如我们对“添加用户”端点的测试所示。
让我们看看另一个服务类AddUserService。这将扩展BaseService,因此它可以访问encode()和issueRequest(),并通过调用AddUserServlet端点返回HttpResponse。
package chapter11.servlets;
import java.io.IOException;
import java.net.http.HttpResponse;
public class AddUserService extends BaseService {
static HttpResponse<String> addUser(
String userName)
throws IOException, InterruptedException {
String path = String.format(
"adduser?userName=%s",
encode(userName)
);
return issueRequest(path);
}
}
Listing 11-18chapter11/src/test/java/chapter11/servlets/AddUserService.java
我们添加一个Post的服务非常相似,尽管我们还没有看到添加帖子的端点。
package chapter11.servlets;
import java.io.IOException;
import java.net.http.HttpResponse;
public class AddPostService extends BaseService {
static HttpResponse<String> addPost(
String title,
String content,
String userName
) throws IOException, InterruptedException {
String path = String.format(
"addpost?title=%s&content=%s&userName=%s",
encode(title),
encode(content),
encode(userName));
return issueRequest(path);
}
}
Listing 11-19chapter11/src/test/java/chapter11/servlets/AddPostService.java
最后,我们还需要一个服务调用来获取个帖子。这是一个执行简单查询的调用——没有任何类型的参数——主要用于早期测试。我们还没有看到它的终结点,但是我们很快就会看到它——不久之后,我们将制作另一个具有更强大功能的“获取帖子”终结点。
package chapter11.servlets;
import java.io.IOException;
import java.net.http.HttpResponse;
public class SimpleGetPostsService extends BaseService{
static HttpResponse<String> getSimplePosts()
throws IOException, InterruptedException {
return issueRequest("simplegetposts");
}
}
Listing 11-20chapter11/src/test/java/chapter11/servlets/SimpleGetPostsService.java
让我们来看看我们提到的两个端点——简单地获取帖子和添加帖子——然后我们来看看第一次将所有这些集合在一起的测试。SimpleGetPostsServlet是第一个,因为它非常简单。
package chapter11.servlets;
import chapter11.dto.PostDTO;
import chapter11.model.Post;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
public class SimpleGetPostsServlet extends ServletBase {
@Override
protected void doGet(
HttpServletRequest req, HttpServletResponse resp
) throws ServletException, IOException {
List<PostDTO> posts = SessionUtil.returnFromSession(session ->
getPosts(session));
write(
resp,
HttpServletResponse.SC_OK,
posts
);
}
private List<PostDTO> getPosts(Session session) {
Query<Post> postQuery = session
.createQuery("from Post p", Post.class);
postQuery.setMaxResults(20);
return postQuery.list().stream().map(post -> {
PostDTO dto = new PostDTO();
dto.setId(post.getId());
dto.setUser(post.getUser().getName());
dto.setContent(post.getContent());
dto.setTitle(post.getTitle());
dto.setCreatedDate(post.getCreateDate());
return dto;
}).collect(Collectors.toList());
}
}
Listing 11-21chapter11/src/main/java/chapter11/servlets/SimpleGetPostsServlet.java
这里唯一需要注意的是将一个Post转换成一个PostDTO的流操作。当然,我们不能没有来源的PostDTO。
package chapter11.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class PostDTO {
int id;
String user;
String title;
String content;
List<CommentDTO> comments=List.of();
LocalDateTime createdDate;
}
Listing 11-22chapter11/src/main/java/chapter11/servlets/PostDTO.java
当然,这个类是一个非常简单的Post的表示。它将comments属性初始化为一个空列表,因为我们希望能够用它来发送回带有评论的帖子…但我们不想被迫这样做。尽管如此,我们也需要包括我们的CommentDTO。
package chapter11.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class CommentDTO {
String user;
String content;
LocalDateTime createdDate;
}
Listing 11-23chapter11/src/main/java/chapter11/servlets/CommentDTO.java
现在我们终于可以开始我们的AddPostServlet了。这个类看起来相当长——将近 70 行——但是它真的非常简单。获取一个User的代码本来可以被抽象掉,但是那个代码只有三条语句(尽管这里取了七行 14 )。在我们得到一个User——如果User不存在就抛出一个隐式异常——之后,我们创建一个Post,用Session.save()将其保存在数据库中,然后创建一个PostDTO来序列化响应。
最后我们进行了一项测试,将所有这些部件组合在一起。首先,让我们看一下测试类,然后我们将遍历它做什么。
package chapter11.servlets;
import chapter11.dto.PostDTO;
import com.fasterxml.jackson.core.type.TypeReference;
import org.testng.annotations.Test;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.List;
import static org.testng.Assert.assertEquals;
public class AddPostServletTest
extends TestBase {
TypeReference<List<PostDTO>> listOfPosts =
new TypeReference<>() {
};
void addPost()
throws IOException, InterruptedException {
HttpResponse<String> response = AddPostService.addPost(
"test post",
"my test post",
"jbo"
);
System.out.println(response.body());
assertEquals(
response.statusCode(),
HttpServletResponse.SC_OK,
"invalid user"
);
PostDTO data =
mapper.readValue(
response.body(),
PostDTO.class
);
response = SimpleGetPostsService.getSimplePosts();
assertEquals(
response.statusCode(),
HttpServletResponse.SC_OK
);
System.out.println(response.body());
List<PostDTO> dtos=mapper.readValue(response.body(),
listOfPosts);
System.out.println(dtos);
assertEquals(dtos.size(), 1);
}
@Test(
expectedExceptions = AssertionError.class,
expectedExceptionsMessageRegExp = "invalid user.*"
)
void addPostNoUser() throws IOException, InterruptedException {
addPost();
}
@Test
void addPostWithValidUser()
throws IOException, InterruptedException {
HttpResponse<String> response =
AddUserService.addUser("jbo");
assertEquals(
response.statusCode(),
HttpServletResponse.SC_OK
);
addPost();
}
}
Listing 11-24chapter11/src/test/java/chapter11/servlets/AddPostServletTest.java
这门课有四个“片段”。首先是一个TypeReference的声明,这样我们可以告诉我们的ObjectMapper在反序列化 JSON 时使用什么类型——我们基本上是在告诉 Jackson 如何创建一个PostDTO引用的List。我们在我们的TestBase.java类中看到了同样的事情,除了我们用了一个Map<String, Object>作为一个更一般化的形式。(这里一个等价的通用的——也是无用的——类型引用可能是TypeReference<List<Map<String, Object>>>,但是我们显然不希望是。我们实际上并没有使用引用引用PostDTO,但是如果我们想这样做,我们已经准备好了。)
我们看到的下一个方法是一个实用方法,实际上是添加一篇文章。这很简单。它调用我们的AddPostService.addPost()方法,然后通过assertEquals检查响应代码,如果断言失败,将“无效用户”添加到失败异常消息中。(我们将在我们的第一个测试方法中看到这一点。)
假设断言通过,它将 JSON 映射到一个PostDTO——我们不使用这个,但是如果我们想要验证创建,我们可以。为了节省空间,我们没有进行全面的验证。
*然后,addPost()方法调用GetSimplePostsServlet,因为我们想确保帖子确实被创建了。(否则,AddPostServlet可能会返回一个新的PostDTO,并且什么都不保留——但仍然通过测试。)我们确保调用返回状态码 200,然后我们将 JSON 映射到一个PostDTO对象的列表中——并验证列表中有一个帖子(我们刚刚创建的帖子)。
我们的第一个测试方法是addPostNoUser(),它只是委托给了一个addPost()方法。顾名思义,当调用addPost()时,没有用户被构造,我们期待一个异常。实际的异常类型将是AssertionError(因为它是由assertEquals()触发的,当我们验证添加帖子的响应状态是 200 时),我们还验证异常消息——因为我们希望通过addPostNoUser()测试,当且仅当因为没有用户在场而抛出异常时。
第二个测试addPostWithValidUser只比addPostNoUser稍微多一点:它首先添加用户,然后调用addPost(),并期望没有任何异常。
那么,我们从这一节中学到了什么?我们已经看到了很多——我们正在构建类来处理对我们构建的 servlets 的实际调用,尽管它们仍然很低级。我们还从概念上构建了我们的 dto 的其余部分,我们已经展示了我们可以适当地添加帖子——我们还展示了如何获取*帖子,尽管这还没有做好。
接下来让我们更好地处理获得帖子的问题。
一个更好的“获取帖子”Servlet
如果你回头看一下Post实体,你会注意到我们有两个过滤器定义:byName和byTerm。是时候我们用这些来创造一个更好的版本了。从结构上来说,这是一样的,但是我们将使用两个可选的请求参数来指定一个“名称”——即文章的作者——或者一个“术语”,即文章标题的简单通配符。
package chapter11.servlets;
import chapter11.dto.PostDTO;
import chapter11.model.Post;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
public class GetPostsServlet extends ServletBase {
@Override
protected void doGet(
HttpServletRequest req, HttpServletResponse resp
) throws ServletException, IOException {
List<PostDTO> posts = SessionUtil.returnFromSession(session ->
getPosts(
session,
req.getParameter("userName"),
req.getParameter("term"))
);
write(
resp,
HttpServletResponse.SC_OK,
posts
);
}
private List<PostDTO> getPosts(
Session session,
String userName,
String term
) {
if (userName != null && !userName.isEmpty()) {
session
.enableFilter("byName")
.setParameter("name", userName);
}
if (term != null && !term.isEmpty()) {
session
.enableFilter("byTerm")
.setParameter("term", "%" + term + "%");
}
Query<Post> postQuery = session
.createQuery(
"from Post p order by p.createDate ",
Post.class
);
return postQuery.list().stream().map(post -> {
PostDTO dto = new PostDTO();
dto.setId(post.getId());
dto.setUser(post.getUser().getName());
dto.setContent(post.getContent());
dto.setTitle(post.getTitle());
dto.setCreatedDate(post.getCreateDate());
return dto;
}).collect(Collectors.toList());
}
}
Listing 11-25chapter11/src/main/java/chapter11/servlets/GetPostsServlet.java
getPosts()方法几乎是同一方法的SimpleGetPostsServlet版本的克隆,加入了一些额外的东西。它接受两个参数,可以是 null 或空的(因此我们不能使用我们的ServletBase.getValidatedParameters()调用),以及Session;然后,它根据这些参数的存在启用各种过滤器。
然后它执行一个简单的查询:from Posts p order by p.createDate,Hibernate 根据过滤器对于这个Session是否有效来应用过滤器。我们不必构建自定义查询或类似的东西。
当然,如果不编写一个测试来展示它,我们就不能拥有这样一个 servlet。我们有一个可能性矩阵要管理,所以我们将再次使用DataProvider。
首先,我们需要一个GetPostsService来匹配我们在其他测试中使用的服务模型。
package chapter11.servlets;
import java.io.IOException;
import java.net.http.HttpResponse;
public class GetPostsService extends BaseService {
static HttpResponse<String> getPosts(String userName, String term)
throws IOException, InterruptedException {
StringBuilder path = new StringBuilder("getposts");
String separator = "?";
if (userName != null && !userName.isEmpty()) {
path
.append(separator)
.append("userName=")
.append(userName);
separator = "&";
}
if (term != null && !term.isEmpty()) {
path
.append(separator)
.append("term=")
.append(term);
}
return issueRequest(path.toString());
}
}
Listing 11-26chapter11/src/test/java/chapter11/servlets/GetPostsService.java
这个类非常简单;它基本上是基于我们的搜索词建立一个查询并发出一个请求。
我们的测试也相当简单:它包含了一个@BeforeMethod,用两个用户(“jbo”和“ts”)填充我们的数据库,还为这些用户添加了五个帖子,在他们之间分配。实际内容并不特别重要;我们只是在寻找一个数据集,我们可以预测我们的测试。
然后我们有一个searchCriteria()方法作为@DataProvider。这里,我们构建了一系列包含四个元素的数组:
-
“搜索用户”值
-
“搜索标题”值
-
给定搜索词的预期记录数
-
对该行所描述内容的描述
我们实际的测试方法非常简单。它从GetPostsService.getPosts()获取一个响应,并验证它是否以一个成功的状态码响应,然后将响应的主体转换成一个List<PostDTO>(就像我们的AddPostServletTest所做的那样),并验证响应的计数是否与数据提供者所说的相符。
package chapter11.servlets;
import chapter11.dto.PostDTO;
import com.fasterxml.jackson.core.type.TypeReference;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.stream.Collectors;
import static org.testng.Assert.assertEquals;
public class GetPostsServletTest
extends TestBase {
TypeReference<List<PostDTO>> listOfPosts =
new TypeReference<>() {
};
@BeforeMethod
void createUsersAndPosts() throws IOException, InterruptedException {
List<Integer> errorCodes = List.of(
AddUserService.addUser("jbo"),
AddUserService.addUser("ts"),
AddPostService.addPost("raccoons 1", "raccoons are cool", "jbo"),
AddPostService.addPost("i like dogs", "see title", "jbo"),
AddPostService.addPost("never seen no cat", "what are cats", "jbo"),
AddPostService.addPost("raccoons 2", "raccoons are trash pandas", "ts"),
AddPostService.addPost("dogs are good", "i named mine scooby", "ts")
)
.stream()
.map(HttpResponse::statusCode)
.filter(status -> status != 200)
.collect(Collectors.toList());
if (errorCodes.size() > 0) {
throw new RuntimeException(
"An error was encountered seeding data"
);
}
}
@DataProvider
Object[][] searchCriteria() {
return new Object[][]{
{null, null, 5, "all posts"},
{"jbo", null, 3, "jbo posts"},
{"jbo", "cat", 1, "jbo cat posts"},
{null, "raccoons", 2, "raccoon posts"},
{"arl", null, 0, "invalid user posts"},
{null, "crow", 0, "search term with no results"},
{"ts", "cat", 0, "ts has no cat posts"}
};
}
@Test(dataProvider = "searchCriteria")
void getPosts(String userName, String term, int count, String desc)
throws IOException, InterruptedException {
HttpResponse<String> response =
GetPostsService.getPosts(userName, term);
assertEquals(
response.statusCode(),
HttpServletResponse.SC_OK
);
List<PostDTO> dtos = mapper.readValue(
response.body(),
listOfPosts);
System.out.println(dtos);
assertEquals(dtos.size(), count);
}
}
Listing 11-27chapter11/src/test/java/chapter11/servlets/GetPostsServletTest.java
完善“应用”
有两个功能我们还没有写,它们都与特定的文章相关。首先,我们没有提供检索特定帖子的方法,也没有提供向帖子添加评论的方法。
我们已经看到了两种镜像功能的流程。毕竟,我们知道如何得到一个User,尽管我们还没有看到将一个User作为一个UserDTO返回,具体地说;获得一个PostDTO就是通过帖子的 id 检索一个PostDTO,并适当地填充它的评论集。向帖子添加评论的过程与为用户添加帖子的过程大致相同。
由于这些过程与我们所看到的非常相似,我们将实现 servlets 并进行一个测试,测试检索给定帖子并检查其评论的机制。
当然,我们已经有了数据传输对象,所以让我们深入 servlet 来获得一个特定的 post。它将接受一个参数,一个帖子 id,并返回一个完全填充的PostDTO(包括评论)。注意,这个名字很像我们的另一个 Servlet:这是GetPostServlet,不是GetPostsServlet。
package chapter11.servlets;
import chapter11.dto.CommentDTO;
import chapter11.dto.PostDTO;
import chapter11.model.Post;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Session;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.stream.Collectors;
public class GetPostServlet extends ServletBase {
@Override
protected void doGet(
HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
try {
Map<String, String> input = getValidatedParameters(req, "id");
Integer id = Integer.parseInt(input.get("id"));
PostDTO postDTO = SessionUtil
.returnFromSession(session -> getPost(session, id));
write(
resp,
HttpServletResponse.SC_OK,
postDTO
);
} catch (Exception e) {
handleException(resp, e);
}
}
protected void handleException(
HttpServletResponse resp,
Exception e
) throws IOException {
if (e.getCause() instanceof ObjectNotFoundException) {
write(
resp,
HttpServletResponse.SC_NOT_FOUND,
Map.of("error", e.getCause().getMessage())
);
} else {
writeError(resp, e);
}
}
protected PostDTO getPost(Session session, Integer id) {
Post post = session.load(Post.class, id);
PostDTO postDTO = new PostDTO();
postDTO.setId(id);
postDTO.setTitle(post.getTitle());
postDTO.setContent(post.getContent());
postDTO.setCreatedDate(post.getCreateDate());
postDTO.setUser(post.getUser().getName());
postDTO.setComments(
post
.getComments()
.stream()
.map(
comment -> {
CommentDTO commentDTO = new CommentDTO();
commentDTO.setContent(comment.getContent());
commentDTO.setCreatedDate(comment.getCreateDate());
commentDTO.setUser(comment.getUser().getName());
return commentDTO;
})
.collect(Collectors.toList())
);
return postDTO;
}
}
Listing 11-28chapter11/src/main/java/chapter11/servlets/GetPostServlet.java
注意getPost()方法,它相当简单,但是有很多代码。它所做的只是由id加载一个Post——如果id不存在,它将抛出一个异常——并用数据填充一个PostDTO,包括将一组注释转换成一列CommentDTO对象的过程。
虽然在catch()块中,doGet()方法值得讨论。handleException()方法的存在是因为SessionUtil.returnFromSession()调用没有像人们希望的那样从 lambda 返回一个ObjectNotFoundException;它实际上抛出了一个RuntimeException,实际的潜在原因是RuntimeException的一部分。
在这种情况下,我们实际上想要返回一个 404——一个 HTTP“未找到”消息——而不是一个“服务器错误”消息,如果用户提交了一个帖子 id,而这个 id可能是正确的却不存在。 15
因此,我们必须做的是在捕获异常时检查异常的原因——如果它是一个ObjectNotFoundException,那么Post的Session.load()失败了,我们希望返回一个 404,而不是 500。
我们要看的下一个 servlet 是AddCommentServlet,它是对 GetPostServlet的扩展——因为我们想要重用我们刚刚看到的getPost()方法。它加载一个User,然后通过id加载Post,然后创建一个简单的Comment并将其添加到Post's现有的注释列表中;然后,它从GetPostServlet返回getPost()的值,以返回填充的PostDTO——此时大部分是从缓存加载的,因为我们始终使用同一个Session。
这里的关键是注意我们如何管理Session;一切都发生在用SessionUtil.returnFromSession()初始化的 lambda 的上下文中,所以缓存是活动的,我们可以完全访问每个对象的数据,因为如果调用发生时它还没有被填充——就像post.getComments().add(comment);—Session可以按需加载它需要的任何东西。更重要的是,由于所有的都是在单个Session中发生的,即使我们试图再次获取,我们的数据也会被缓存。
*我们可以编写简单明了的代码,并且我们可以利用高速缓存的优势使其快速运行,尽管由于安装和拆卸时间的原因,我们的测试并不是特别快。
这是我们的AddCommentServlet。
package chapter11.servlets;
import chapter11.dto.CommentDTO;
import chapter11.dto.PostDTO;
import chapter11.model.Comment;
import chapter11.model.Post;
import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.query.Query;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.stream.Collectors;
public class AddCommentServlet extends GetPostServlet {
@Override
protected void doGet(
HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
try {
Map<String, String> input = getValidatedParameters(
req,
"id",
"userName",
"content"
);
Integer id = Integer.parseInt(input.get("id"));
PostDTO postDTO = SessionUtil
.returnFromSession(session ->
addComment(
session,
id,
input.get("userName"),
input.get("content")
)
);
write
(resp,
HttpServletResponse.SC_OK,
postDTO
);
} catch (Exception e) {
handleException(resp, e);
}
}
PostDTO addComment(
Session session,
Integer id,
String userName,
String content
) {
Query<User> userQuery = session.createQuery(
"from User u where u.name=:name",
User.class
);
userQuery.setParameter("name", userName);
User user = userQuery.getSingleResult();
Post post = session.load(Post.class, id);
Comment comment = new Comment();
comment.setUser(user);
comment.setPost(post);
comment.setContent(content);
comment.setCreateDate(LocalDateTime.now());
session.save(comment);
post.getComments().add(comment);
return getPost(session, id);
}
}
Listing 11-29chapter11/src/main/java/chapter11/servlets/AddCommentServlet.java
当然,我们需要进行测试。这也意味着我们有一些服务代理来使服务调用更容易阅读。因此,我们先来看看GetPostService、AddCommentService,然后是AddCommentServletTest。
谢天谢地,服务时间相当短。
package chapter11.servlets;
import java.io.IOException;
import java.net.http.HttpResponse;
public class AddCommentService extends BaseService {
static HttpResponse<String> addComment(
Integer id,
String content,
String userName
) throws IOException, InterruptedException {
String path = String.format(
"addcomment?id=%s&content=%s&userName=%s",
id,
encode(content),
encode(userName));
return issueRequest(path);
}
}
Listing 11-31chapter11/src/test/java/chapter11/servlets/AddCommentService.java
package chapter11.servlets;
import java.io.IOException;
import java.net.http.HttpResponse;
public class GetPostService extends BaseService {
static HttpResponse<String> getPost(Integer id)
throws IOException, InterruptedException {
return issueRequest(
String.format("getpost?id=%d", id)
);
}
}
Listing 11-30chapter11/src/test/java/chapter11/servlets/GetPostService.java
现在我们开始测试中最有趣的部分(好吧,如果这样的事情很有趣的话):第AddCommentServletTest。
有两个测试。设置很简单:createUsersAndPosts()方法设置两个用户和一篇文章并保存文章,这样我们就可以使用它的id来添加评论。
第一个测试testAddComment()加载Post并验证它没有注释,因为它不应该有注释!然后添加一个注释,并验证从AddCommentServlet返回的PostDTO有一个注释;然后,它重复这个过程来验证我们可以按顺序添加多个注释。
最后,它通过GetPostServlet再次加载Post,以确保结果与来自AddCommentServlet的调用相同。
第二个测试稍微简单一点——它获取一个不应该存在的帖子,使用存在的帖子的id作为派生新的id的基础。
package chapter11.servlets;
import chapter11.dto.PostDTO;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpResponse;
import static org.testng.Assert.assertEquals;
public class AddCommentServletTest extends TestBase {
PostDTO post = null;
@BeforeMethod
void createUsersAndPosts()
throws IOException, InterruptedException {
AddUserService.addUser("jbo");
AddUserService.addUser("ts");
HttpResponse<String> postData =
AddPostService.addPost("raccoons", "raccoons are neat", "jbo");
// this is how we get the post's id.
post = mapper.readValue(postData.body(), PostDTO.class);
}
@Test
void testAddComment() throws IOException, InterruptedException {
HttpResponse<String> response =
GetPostService.getPost(post.getId());
validatePost(response, 0);
response = AddCommentService.addComment(
post.getId(),
"what's the deal with raccoons, really",
"ts"
);
assertEquals(response.statusCode(), 200);
validatePost(response, 1);
response = AddCommentService.addComment(
post.getId(),
"they're the coolest",
"jbo"
);
assertEquals(response.statusCode(), 200);
validatePost(response, 2);
response =
GetPostService.getPost(post.getId());
validatePost(response, 2);
}
@Test
void testInvalidGetPost()
throws IOException, InterruptedException {
HttpResponse<String> response =
GetPostService.getPost(post.getId() + 1);
assertEquals(
response.statusCode(),
HttpServletResponse.SC_NOT_FOUND
);
}
void validatePost(
HttpResponse<String> response,
int commentSize
) throws IOException {
assertEquals(response.statusCode(), 200);
PostDTO retrieved = mapper.readValue(response.body(), PostDTO.class);
assertEquals(retrieved.getComments().size(), commentSize);
assertEquals(retrieved.getTitle(), "raccoons");
}
}
Listing 11-32chapter11/src/test/java/chapter11/servlets/AddCommentServletTest.java
摘要
这是一个巨大的篇章!我们用它来演示如何将 Hibernate 集成到一个或多或少可以工作的管理博客的 web 应用中;这不是一个好的应用,但是可以作为一个应用的基础。
我们学到了很多东西:我们已经了解了如何设置一个嵌入式 servlet 引擎(Undertow ),我们已经了解了会话划分和一种很好地管理它的方法,我们还了解了整个过程的一组相当详尽的测试。
不得不说,这一章中的大部分代码并不特别是有用;有了足够的努力和意图,可以让变得有用,但是我们在这里真正寻找的是理解一些关于跨架构墙传递数据的问题,例如,从一个实体到 JSON。
我们接下来的章节将集中在将 Hibernate 集成到一个很可能在现实世界中遇到的框架中。
********十二、集成 Hibernate
在第十一章中,我们展示了一种将 Hibernate 集成到 servlet 应用中的技术,但是我们的应用对于 JVM 来说是非常“裸机”的。没有人会写这样的应用。相反,人们使用应用框架来处理我们不得不自己处理的许多问题,比如 Quarkus、Spring Data(在我们的例子中,特别是 Spring Data JPA 模块)或 ActiveJ。在这一章中,我们将看看 Hibernate 与这三个平台的集成,这将让我们更好地了解 Hibernate 在“真实世界”中的使用情况,并且我们将看到我们到目前为止学到的经验有多少仍然适用。
不过,这里有些困难。
hibernate 6——本书的主题——在撰写本文时,仍然是非常新的,应用框架作者在集成新版本时有一个隐含的延迟。因此,对 Hibernate 的框架支持往往落后于 Hibernate 本身,这是必然的。
例如,Quarkus 与 Hibernate 的内部有非常紧密的绑定,可以在许多不同的环境下提供优化(甚至超越 JVM)。在内部绑定上投入了大量的工时,如果 Hibernate 6 仍在开发中,Quarkus 团队在有稳定的 API 之前投入大量精力是不明智的。
因此,在本章中,我们将在必要的地方使用 Hibernate 5。
这在很大程度上是可以接受的因为当框架获得【Hibernate 6 支持的时候——可能到你读到这篇文章的时候——集成看起来几乎是一样的,如果不是完全一样的话。**
这一章是关于集成 Hibernate,而不是 Hibernate 6——尽管我们会尽可能地使用 Hibernate 6。我们将会看到生成项目的大量代码和过程,我们的项目结构将会有相当多的重复工作,所以请做好准备。
春天
我们的第一次融合是与春天( https://spring.io/projects/spring-framework )的融合。Spring 是一个围绕依赖注入提供服务的框架,这是一个鼓励关注点清晰分离的架构设计:如果一个类需要一个资源,它就声明对它的依赖(通常基于接口),并且框架提供了一个提供依赖的简单方法。
例如,假设我们有一个需要访问采购订单来构建报告的类。该类不会(或者不应该)关心采购订单来自哪里;它只需要能够访问采购订单。通过依赖注入,我们可以创建一个接口,也许是一个PurchaseOrderAccessor,并在一个PurchaseOrderAccessor上声明一个依赖。
在测试期间,我们可以提供一个实现,返回从 JSON 文件填充的数据,或者手动构造数据,例如,这意味着没有 Hibernate,没有数据库,没有任何不可预测的东西,这就形成了一个理想的测试框架:您将能够确切地指定数据看起来是什么样子的,因此来自采购订单报告的输出将是绝对可预测的。这被称为功能测试或单元测试。 2
当然,从逻辑上讲,您还会有一个访问数据库的PurchaseOrderAccessor实现。在这里,Hibernate 可能是完全合适的,这个类也应该被彻底测试,但是这通常是一个集成测试。(这里的界限通常很模糊,很多程序员会混淆集成和功能测试。)集成测试是跨越架构边界的测试,比如在应用和它的数据存储机制之间。
这本书强调了集成测试的最终结果,因为它关注的是 Hibernate。你可能会说,数据库是与领域相适应的。
回到春天!Spring 可能是 Java 中最流行的依赖注入框架;它有一个相当简单的声明性语法,还有一个巨大的生态系统。
将 Hibernate 集成到 Spring 有几种方法,我们无法一一介绍;我们将首先介绍一个更简单的,提供直接 Hibernate 访问(因此看起来非常类似于我们在整本书中看到的代码。)
我们首先要做的是定义一个包含五个模块的伞状项目(就像这本书到目前为止有一个顶层项目,每个章节都有模块)。然后,我们将定义一个ch12common项目,并使用它来保存一些我们将在这一章节的剩余部分中重用的资源,最后我们将深入到 Spring 集成中。
首先是chapter12项目,它主要组织其他模块。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>hibernate-6-parent</artifactId>
<version>5.0</version>
</parent>
<packaging>pom</packaging>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter12</artifactId>
<modules>
<module>ch12common</module>
<module>activej</module>
<module>spring</module>
<module>springboot</module>
</modules>
</project>
Listing 12-1chapter12/pom.xml
现在让我们深入到ch12common项目,它将有一个非常简单的“博客”项目的对象模型——由一个实体、一个Post—以及一个用于处理Post对象的接口(以及一个用于处理 Hibernate 的实现,尽管我们不会在本章的每个项目中使用这个实现。就此而言,我们也不会在每个部分都使用这个“公共”项目;我们将根据需要进行挑选)。
下面是ch12common项目模型。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>chapter12</artifactId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ch12common</artifactId>
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.0.0.Alpha8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
</project>
Listing 12-2chapter12/ch12common/pom.xml
这里有几点需要注意。首先,Hibernate 是作为一个provided依赖项包含进来的,这意味着它在这个编译单元的类路径中,但不是传递依赖项。这意味着任何使用ch12common的项目都需要为自己提供 Hibernate。
我们这样做是因为我们在类路径中需要 Hibernate,但是我们不想告诉其他项目哪个 Hibernate 版本的使用。如果其他项目使用的 Hibernate 版本没有使用与 Hibernate 6 相同的类结构,这里就有潜在的不兼容,尽管在撰写本文时我们是安全的。
包括的其他依赖项——H2 和 log back——是可传递的依赖项,因此它们将被包括在任何使用ch12common的类路径中。
回到项目上来!我们也有 Hibernate 的配置文件。
<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:./activej</property>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<property name="use_sql_comments">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create-drop</property>
<mapping class="ch12.Post"/>
</session-factory>
</hibernate-configuration>
Listing 12-3chapter12/ch12common/src/main/resources/hibernate.cfg.xml
这是我们的Post实体。为了举例而写,它大部分是自动生成的。3
package ch12;
import javax.persistence.*;
import java.util.Date;
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column(nullable = false, unique = true)
String title;
@Column(nullable = false)
@Lob
String content;
@Temporal(TemporalType.TIMESTAMP)
Date createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
@Override
public String toString() {
return "Post{" +
"id=" + id +
", title='" + title + '\'' +
", content='" + content + '\'' +
", createdAt=" + createdAt +
'}';
}
}
Listing 12-4chapter12/ch12common/src/main/java/ch12/Post.java
注意我们对createdAt字段使用了Date。通常,我们最好使用OffsetDateTime,但是在最近的 Java 中集成新的日期-时间 API 对于一些旧的库来说偶尔会有问题;如果我们没有在多个项目中使用一个公共的实用程序库,我们会“正确地”这样做,而不是在这里使用Date。 4
我们的下一个类是一个PostManager,这个接口仅仅指定一个实现可以提供一个帖子列表并可以保存一个帖子。在一个“真正的应用”中,我们想要提供分页、访问单个帖子、更新帖子的方法,可能还有删除帖子的方法——典型的 CRUD 类型操作——但是我们在本书的其余部分已经看到了这些例子,在这里它们是不必要的。 5
package ch12;
import java.util.List;
public interface PostManager {
List<Post> getPosts();
Post savePost(String title, String content);
}
Listing 12-5chapter12/ch12common/src/main/java/ch12/PostManager.java
我们ch12common的最后一节课是一节HibernatePostManager。这个类复制了我们从util项目的SessionUtil中看到的一些代码——在returnFromSession()方法中——并实现了PostManager接口。它也没有创建SessionFactory来获取Session——当使用这个类时,我们将在我们的每个集成模块中这样做。
package ch12;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
public class HibernatePostManager implements PostManager {
private final SessionFactory sessionFactory;
public HibernatePostManager(SessionFactory factory) {
this.sessionFactory = factory;
}
@Override
public List<Post> getPosts() {
return returnFromSession(session -> {
Query<Post> postQuery = session.createQuery(
"from Post p order by p.createdAt desc",
Post.class
);
postQuery.setMaxResults(20);
return postQuery.list();
});
}
@Override
public Post savePost(String title, String content) {
return returnFromSession(session -> {
Post post = new Post();
post.setTitle(title);
post.setContent(content);
post.setCreatedAt(new Date());
session.save(post);
return post;
});
}
public <T> T returnFromSession(Function<Session, T> command) {
try (Session session = sessionFactory.openSession()) {
Transaction tx = null;
try {
tx = session.beginTransaction();
return command.apply(session);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (tx != null) {
if (tx.isActive() &&
!tx.getRollbackOnly()) {
tx.commit();
} else {
tx.rollback();
}
}
}
}
}
}
Listing 12-6chapter12/ch12common/src/main/java/ch12/HibernatePostManager.java
现在我们可以开始看实际的弹簧积分了。
我们的 Spring 应用将会非常简单:简单地存储一个Post并检索它。我们的其他应用将为此提供一个 web 界面,但 Spring 本身对此有点简单;编写我们自己的 web 集成和部署层是相当多的代码,除了占用空间之外,实际上并没有做更多的事情。
我们的 Spring 应用的职责相当简单:它需要创建一个SessionFactory来提供给我们的HibernatePostManager,以及提供HibernatePostManager本身。它还需要创建一种机制,通过这种机制我们可以协调 Spring 组件中的事务。
首先,让我们看看项目模块本身,然后我们来看看代码。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>chapter12</artifactId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>spring</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>ch12common</artifactId>
<version>5.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.0.0.Alpha8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-hikaricp</artifactId>
<version>6.0.0.Alpha8</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<mainClass>ch12.Main</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
</project>
Listing 12-7chapter12/spring/pom.xml
这个项目相当简单;它导入了ch12common模块(这意味着它获得了 H2 和 Logback ),然后导入了 Hibernate 本身和两个 Spring 依赖项:spring-orm(它通过一些方便的包装类提供了 Spring 与 Hibernate 的接口)和spring-context,后者为我们提供了用于配置的基本注释。
去编码!
我们将有一个庞大的 one 类来完成所有这些工作。它将创建一个ApplicationContext类——这是我们进入 Spring 资源的入口点——并从上下文中请求一个PostManager,并与那个PostManager进行交互。它还将声明它需要的资源:一个LocalSessionFactoryBean资源(它提供了SessionFactory)、PlatformTransactionManager和PostManager本身。
package ch12;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class Main {
@Bean
LocalSessionFactoryBean sessionFactory() {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setConfigLocation(new ClassPathResource("/hibernate.cfg.xml"));
return sessionFactory;
}
@Bean
public PlatformTransactionManager hibernateTransactionManager() {
HibernateTransactionManager transactionManager
= new HibernateTransactionManager();
transactionManager.setSessionFactory(sessionFactory().getObject());
return transactionManager;
}
@Bean
PostManager postManager(SessionFactory factory) {
return new HibernatePostManager(factory);
}
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Main.class);
ApplicationContext context =
new AnnotationConfigApplicationContext(Main.class);
PostManager postManager = context.getBean(PostManager.class);
logger.info(postManager.toString());
postManager.savePost("foo", "bar");
logger.info(postManager.getPosts().toString());
}
}
Listing 12-8chapter12/spring/src/main/java/ch12/Main.java
Spring 在所有这些方面的威力可以在返回一个PostManager的方法的声明中看到。我们用@Bean对其进行注释——建议 Spring 应该提供一个PostManager的实例作为 Spring 管理的对象——并且我们需要一个SessionFactory参数。
Spring 将寻找另一个托管实例,该实例返回与 a SessionFactory兼容的,并在调用该方法获取PostManager时由注入。这也是一个简单、标准的方法;没有什么可以阻止我们手动调用它,但是从 Spring 获得它意味着我们获得了以这样一种方式构建的东西,它需要的一切都已经为它提供了。
我们在这里使用 Hibernate Session,但是您也可以轻松地使用 JPA EntityManager方法。你可以使用一个LocalContainerEntityManagerFactoryBean——多好的名字——以及其他一些不同的方法,但是虽然类名和接口会改变,但是过程基本上保持不变。
main()方法相当简单,尽管乍一看令人困惑:它只是使用一个类来构建一个ApplicationContext,该类扫描用注释声明的资源。然后它获取一个满足它需要的定义的实例(“给我一个是PostManager的实例”),并使用它来保存一个Post并列出它能找到的Post实体。
当然,您也可以更加声明性地设置 Hibernate 配置;这里,我们使用的是我们已经反复使用过的 XML 配置,但这并不意味着您不能将 XML 配置名称作为资源提供,甚至也不能以声明方式参数化实际的配置。
由于 Maven 加载资源的方式,运行这个需要一点点的参与。首先运行mvn install将项目及其依赖项安装到本地 Maven 存储库中,然后,由于使用了exec-maven-plugin,运行mvn exec:java来执行带有项目依赖项的ch12.Main类。看着也不是特别刺激;我们将在本章的后面部分获得更有用的诊断信息。
我们接下来的部分将设置与 HTTP 端点的集成,因此我们实际上可以通过浏览器或类似于curl或Postman的实用程序与框架进行交互。
Spring Boot 的春季数据
我们的下一个集成是与 Spring Boot ( https://spring.io/projects/spring-boot ),利用 Spring 数据项目( https://spring.io/projects/spring-data ),更具体地说,Spring 数据 JPA ( https://spring.io/projects/spring-data-jpa )。
Spring Data 将实际的数据访问抽象成一组被称为存储库的接口,这在很大程度上是与数据库无关的。当然,我们这里针对的是 JPA(和 Hibernate ),但是你可以简单地针对 MongoDB、JDBC、Redis、Neo4j,或者……任何其他支持的数据库,主要的变化将是数据源的配置,尽管在功能上有一些差异。
我们还将利用 Spring Web,所以我们将提供一个 REST 端点(很像我们在第十一章中看到的,除了代码少得多)。
当然,我们的项目模型是第一个。我们将包括ch12common模块,但是我们从该模块中利用的唯一的是Post实体本身。spring-boot-starter-data-jpa依赖项将为我们包含 Hibernate 5 6 ,默认情况下,Spring Boot 还将为我们填充一个对 H2 数据库的引用。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>springboot</artifactId>
<version>1.0.0</version>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>ch12common</artifactId>
<version>5.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Listing 12-9chapter12/springboot/pom.xml
我们的下一个类型实际上是我们的Repository接口。在表面之下还有很多事情要做,但简单来说,Spring 将创建一个代理,根据类的定义,为您提供许多标准化的创建、读取、更新和删除方法。对于我们所需要的,我们已经在JPARepository接口中定义了方法:findAll()和save()。我们需要做的就是创建一个接口,为JPARepository、实体类型(Post)和实体的主键类型(Long)提供类型。
package ch12;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository
extends JpaRepository<Post, Long> {
}
Listing 12-10chapter12/springboot/src/main/java/ch12/PostRepository.java
在春季数据中有一个 lot 我们没有使用。我们实际上可以在我们的接口中定义查询,例如,从方法名中推断出查询,但这是一个需要自己的书的主题,是的,Apress 有多种优秀的资源,可以向您展示比这个简单示例更多的关于 Spring 数据的信息。
我们的下一个类是PostController,它利用 Spring Web 通过 HTTP 提供端点。它被标注为一个@RestController,它的构造函数需要一个PostRepository——所以 Spring 将寻找一个PostRepository并为我们适当地构造它。(您还可以进一步抽象:您可以让一个控制器利用一个服务,该服务本身利用多个存储库与您的数据源进行交互。这一章充满了纯学术概念,所以为了集中在示例配置上,我们没有完全充实内容。)
我们在这里声明了两个端点:一个在/,它获取最近帖子的列表,另一个在/add,它允许我们添加一个帖子。两者都是通过 HTTP GET来利用的,这不是很明智,但是我们并不想展示 Spring Web 的理想用法;正确地做它会在控制器级别引入大量的验证代码,这会妨碍我们的工作。如果您愿意,您可以通过将注释更改为@RequestMapping来支持POST(它将处理多个 HTTP 动词),或者更改为@PostMapping,但是使用POST将意味着以不同的方式处理内容,对于针对 Spring Web 的书来说,这是一个更好的主题。
package ch12;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
@RestController
public class PostController {
private final PostRepository postRepository;
PostController(PostRepository postRepository) {
this.postRepository = postRepository;
}
@GetMapping(value = "/", produces = {"application/json"})
public List<Post> index() {
return postRepository.findAll(
Sort.by(Sort.Direction.DESC,"createdAt")
);
}
@GetMapping(value = "/add", produces = {"application/json"})
public Post addPost(
@RequestParam("title") String title,
@RequestParam("content") String content) {
Post post = new Post();
post.setTitle(title);
post.setContent(content);
post.setCreatedAt(new Date());
postRepository.save(post);
return post;
}
}
Listing 12-11chapter12/springboot/src/main/java/ch12/PostController.java
我们的最后一个类是PostApplication——它将所有的东西联系在一起——但它所做的只是作为一个入口点。Spring Boot 扫描类路径寻找它需要的资源,并根据它找到的资源启动进程,所以当它找到PostRepository时,它知道初始化数据库和相关资源——当然包括 Hibernate——当它找到PostController时,它启动一个嵌入式 web 服务器。
package ch12;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PostApplication {
public static void main(String[] args) {
SpringApplication.run(PostApplication.class, args);
}
}
Listing 12-12chapter12/springboot/src/main/java/ch12/PostApplication.java
我们可以在启动ch12.PostApplication(可能用mvn spring-boot:run)后,用curl来测试,使用以下命令:
> curl -s -w "\n" http://localhost:8080/
[]
> curl -s -w "\n" "http://localhost:8080/add?title=foo&content=bar"
{"id":1,"title":"foo","content":"bar","createdAt":"2021-07-24T17:02:58.042+00:00"}
> curl -s -w "\n" http://localhost:8080/
[{"id":1,"title":"foo","content":"bar","createdAt":"2021-07-24T17:02:58.042+00:00"}]
我们使用-s来关闭告诉我们进度的curl;否则,您会得到一个颇有启发性的图表,显示该实用程序检索少于 300 字节的数据的速度,这是没有用的。我们还使用-w "\n"在内容显示后添加一个新行,因为否则我们的下一个提示会在请求输出后立即显示。
这些都有时间戳(猜猜这是什么时候运行的!),但是您可以随意使用端点,看看输出有什么不同。
Spring Boot 有很大的可配置性;在这里,我们依赖于许多元素的默认值,这不适合“真正的应用”与 Spring 一样,Apress 拥有利用 Spring Boot 生态系统的多种资源;在这里,您可以看到 Hibernate 的集成是多么简单。
同样,在撰写本文时,这是 Hibernate 5 集成,而不是 Hibernate 6 集成,但是在您阅读本文时,他们可能已经完成了向 Hibernate 6 的迁移。
ActiveJ
ActiveJ ( https://activej.io/ )是一个专注于高性能内容交付的替代平台。与 Spring Boot 不同,它不太依赖企业空间的传统 Java 架构模式。 7 它倾向于关注微控制器和异步进程,以获得出色的性能。
异步设计,或“反应式编程”,指的是设计在数据流上操作的过程,而不是在编程中更传统的调用-响应模型上操作的过程。反应式编程倾向于尽可能避免有副作用的代码,它有自己的操作模式。不过,我们在这里不打算关注 Hibernate 的反应式模型;和春天一样,这样一个主题想要自己的书。
与 Spring 非常相似,ActiveJ 使用@Provides注释扫描基于类型的可注入资源的类路径,它也可以基于类型注入引用。
不过,对于 web 资源,ActiveJ 使用一个RoutingServlet将资源按类型和路径映射到 lambdas。实际的 lambda 本身并不特别复杂,尽管它们在RoutingServlet中的使用可以创建一些有趣的数据结构,因为 lambda 只接收request引用。
首先,像往常一样,让我们看看项目模型。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>chapter12</artifactId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>activej</artifactId>
<dependencies>
<dependency>
<groupId>io.activej</groupId>
<artifactId>activej-launchers-http</artifactId>
<version>4.3</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.core.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>ch12common</artifactId>
<version>5.0</version>
</dependency>
</dependencies>
</project>
Listing 12-13chapter12/activej/pom.xml
和本章中的其他例子一样,我们包括了ch12common,它提供了Post、PostManager和HibernatePostManager,以及一个有效的休眠配置。我们包括 Hibernate 6,因为它没有固有的 Hibernate 配置(这也是我们在 Spring 中看到的),以及 ActiveJ 依赖本身。
我们还包括了 Jackson(正如我们在第十一章中看到的)和jackson-datatype-jsr310模块,它允许我们将Date引用序列化为人类可读的日期而不是数字。(我们的其他模块会为我们完成这项工作,而无需我们进行任何干预。)
在我们把所有的东西绑在一起之前,让我们看看我们的资源。
我们拥有的第一个资源是ObjectMapperFactory。Jackson 的ObjectMapper不是 threadsafe,它占用的资源很少;使用它的首选方式是在使用时创建一个新的。也就是说,我们对自己的有特定的要求:我们希望它不序列化空引用(如果一个数据字段是空的,我们不想看到它),和我们希望将日期序列化为字符串而不是数字。
package ch12;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
public class ObjectMapperFactory {
public ObjectMapper buildMapper() {
ObjectMapper mapper = new ObjectMapper()
.setSerializationInclusion(
JsonInclude.Include.NON_NULL
)
.disable(
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
);
mapper.registerModule(new JavaTimeModule());
return mapper;
}
}
Listing 12-14chapter12/activej/src/main/java/ch12/ObjectMapperFactory.java
下一个类有两个方法,大致类似于 servlets。这两种方法都接受一个 ActiveJ HttpRequest并返回一个HttpResponse,根据需要将数据映射成适当的形式。在建造方面,它需要一艘ObjectMapperFactory来建造一艘ObjectMapper和一艘PostManager。
package ch12;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.activej.http.HttpRequest;
import io.activej.http.HttpResponse;
import java.util.List;
public class Endpoints {
PostManager postManager;
ObjectMapperFactory mapperFactory;
public Endpoints(
PostManager postManager,
ObjectMapperFactory mapperFactory
) {
this.postManager = postManager;
this.mapperFactory = mapperFactory;
}
HttpResponse getPosts(HttpRequest request) {
try {
List<Post> posts = postManager.getPosts();
return HttpResponse
.ok200()
.withJson(mapperFactory
.buildMapper()
.writeValueAsString(posts)
);
} catch (JsonProcessingException e) {
return HttpResponse
.ofCode(500)
.withPlainText(e.getMessage());
}
}
HttpResponse addPost(HttpRequest request) {
String title = request.getQueryParameter("title");
String content = request.getQueryParameter("content");
try {
Post post = postManager.savePost(title, content);
return io.activej.http.HttpResponse
.ok200()
.withJson(mapperFactory
.buildMapper()
.writeValueAsString(post)
);
} catch (JsonProcessingException e) {
return io.activej.http.HttpResponse
.ofCode(500)
.withPlainText(e.getMessage());
}
}
}
Listing 12-15chapter12/activej/src/main/java/ch12/Endpoints.java
现在让我们把所有东西绑在一起。我们的PostApp实际上做了和我们在 Spring 和 Spring Boot 例子中看到的一样的事情;它声明了许多方法来按类型返回特定的资源,用@Provides进行了注释,并使用找到的任何资源启动 HTTP 服务器。在我们的例子中,是一个RoutingServlet,它将 URL 发送到我们的Endpoints类中的各种方法。
package ch12;
import io.activej.http.AsyncServlet;
import io.activej.http.RoutingServlet;
import io.activej.inject.annotation.Provides;
import io.activej.launcher.Launcher;
import io.activej.launchers.http.HttpServerLauncher;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import static io.activej.http.HttpMethod.GET;
public class PostApp
extends HttpServerLauncher {
@Provides
ObjectMapperFactory mapper() {
return new ObjectMapperFactory();
}
@Provides
SessionFactory sessionFactory() {
StandardServiceRegistry registry =
new StandardServiceRegistryBuilder()
.configure()
.build();
SessionFactory factory = new MetadataSources(registry)
.buildMetadata()
.buildSessionFactory();
return factory;
}
@Provides
PostManager getPostManager(SessionFactory factory) {
return new HibernatePostManager(factory);
}
@Provides
Endpoints endpoints(
PostManager manager,
ObjectMapperFactory mapperFactory
) {
return new Endpoints(manager, mapperFactory);
}
@Provides
AsyncServlet servlet(
Endpoints endpoints
) {
return RoutingServlet.create()
.map(GET, "/", endpoints::getPosts)
.map(GET, "/add", endpoints::addPost);
}
public static void main(String[] args) throws Exception {
Launcher launcher = new PostApp();
launcher.launch(args);
}
}
Listing 12-16chapter12/activej/src/main/java/ch12/PostApp.java
和 Spring Boot 的例子一样,我们可以用curl进行简单的测试。注意,最后一个命令被输入到另一个命令jsonpp,它为我们格式化 JSON:8
> curl -s -w "\n" http://localhost:8080/
[]
> curl -s -w "\n" "http://localhost:8080/add?title=foo&content=bar"
{"id":1,"title":"foo","content":"bar","createdAt":"2021-07-24T17:09:36.125+00:00"}
> curl -s -w "\n" "http://localhost:8080/add?title=baz&content=bletch"
{"id":2,"title":"baz","content":"bletch","createdAt":"2021-07-24T17:09:42.498+00:00"}
> curl -s -w "\n" http://localhost:8080/ | jsonpp
[
{
"id": 2,
"title": "baz",
"content": "bletch",
"createdAt": "2021-07-24T17:09:42.498+00:00"
},
{
"id": 1,
"title": "foo",
"content": "bar",
"createdAt": "2021-07-24T17:09:36.125+00:00"
}
]
ActiveJ 在框架大战中是一个相对较新的入口,但它是专门为可伸缩性和耐看性而设计的。
第四的
Quarkus 是一个以开发人员易用性为目标的框架,同时可以作为一个本机映像轻松部署,这使得它非常适合云环境。它依赖一组扩展点进行优化,因此需要一些努力来支持更新的技术和版本,如 Hibernate 6。我们将利用 Quarkus 生态系统,但这意味着我们将瞄准 Hibernate 5(就像我们对 Spring Boot 所做的那样),直到生态系统支持 Hibernate 6;当发生时,Hibernate 6 集成很可能就像这里展示的一样无缝。
从开发者的角度来看,Quarkus 非常好。有一个 Maven 命令可以创建一个 Quarkus 项目,它为在 JVM 下运行的应用提供了一个快速的重新编译周期,如果您已经满足了这样做的系统要求,还可以选择使用 Maven 概要文件构建一个本机映像。 9
构建能够访问 Hibernate 的 Quarkus 应用的基础相当简单。然而,我们将以不同的方式处理这个项目,所以我们可以利用工具;我们将创建一个不依赖于本书中任何其他内容的项目。
在书中的源代码中,这个项目位于chapter12/quarkus下;它是而不是书中任何其他项目的子模块。它是独立的。
我们要做的第一步是创建一个 Quarkus 项目,命令如下:
mvn io.quarkus:quarkus-maven-plugin:2.0.2.Final:create \
-DprojectGroupId=com.autumncode.books.hibernate \
-DprojectArtifactId=quarkus \
-DclassName="ch12.HelloWorld" \
-Dpath="/hello"
这将创建一个quarkus目录,带有一个 Maven 包装器和一个pom.xml。pom.xml相当长,将完全标准化;我们根本不需要修改它。
通过切换到quarkus目录并运行以下命令,我们已经可以运行这个项目了:
mvn quarkus:dev
这将编译生成的应用,并在端口 8080(默认)启动一个 web 服务器:当我们创建应用时,我们告诉它在/hello放置一个端点,我们的应用一开始就可以做以下事情:
> curl -s -w "\n" http://localhost:8080/hello
Hello RESTEasy
是我们集成 Hibernate 的时候了。
为了做到这一点,我们想要添加两个扩展,开发这些模块是为了帮助 Quarkus 高效地集成到 Hibernate 中。这在很大程度上是为了让 Quarkus 不仅能在 JVM 中优化执行,还能在本地环境中优化执行;我们不打算在这里利用本机执行,但提供了可能性。
第一个扩展是针对 Hibernate 本身的,我们希望为 H2 数据库添加另一个扩展。我们可以用 Quarkus 工具做到这一点:
mvn quarkus:add-extension -Dextensions="quarkus-jdbc-h2,quarkus-hibernate-orm"
如果您愿意,您可以使用mvn quarkus:list-extensions查看所有可用的扩展。
我们可以重用ch12common类中的Post.java;这和我们看到的来源完全一样。
package ch12;
import javax.persistence.*;
import java.util.Date;
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Column(nullable = false, unique = true)
String title;
@Column(nullable = false)
@Lob
String content;
@Temporal(TemporalType.TIMESTAMP)
Date createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
@Override
public String toString() {
return "Post{" +
"id=" + id +
", title='" + title + '\'' +
", content='" + content + '\'' +
", createdAt=" + createdAt +
'}';
}
}
Listing 12-17chapter12/quarkus/src/main/java/ch12/Post.java
我们也可以重用PostManager和HibernatePostManager,或者接近于此,但是我们实际上并不需要这么做,没有它们我们实际上可以有更简单的代码。这主要是因为 Quarkus 将为我们管理事务,我们想添加一个注释来通知 Quarkus 我们需要什么资源。
package ch12;
import java.util.List;
public interface PostManager {
Post savePost(Post post);
List<Post> getPosts();
}
Listing 12-18chapter12/quarkus/src/main/java/ch12/PostManager.java
用@ApplicationScoped将HibernatePostManager注释为 Quarkus 的托管 bean,并通过@Inject注释接收休眠Session;我们将每个方法标记为@Transactional,因此我们有一个自然的(和强制的)事务边界。(这就是我们重新实现该类的原因;有了 Quarkus 的事务管理,我们不需要很多样板代码来处理事务。)除此之外,实际的执行代码与我们在本章其他地方看到的代码非常相似。
package ch12;
import org.hibernate.Session;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.TypedQuery;
import javax.transaction.Transactional;
import java.util.Date;
import java.util.List;
@ApplicationScoped
public class HibernatePostManager implements PostManager {
@Inject
Session session;
@Transactional
@Override
public Post savePost(Post post) {
post.setCreatedAt(new Date());
session.save(post);
return post;
}
@Override
@Transactional
public List<Post> getPosts() {
TypedQuery<Post> postQuery = session
.createQuery(
"select p from Post p order by p.createdAt desc",
Post.class
);
postQuery.setMaxResults(20);
return postQuery.getResultList();
}
}
Listing 12-19chapter12/quarkus/src/main/java/ch12/HibernatePostManager.java
对于应用的持久化方面,只剩下一部分需要配置:我们要告诉 Quarkus 我们的数据库。我们可以在src/main/resources中用application.properties来做这件事。如果我们需要的话,我们在这里有相当多的控制权(参见 https://quarkus.io/guides/hibernate-orm#hibernate-configuration-properties 获取完整的属性列表),但是在大多数情况下,我们只想告诉 Quarkus 如何连接到数据库以及如何管理模式。在这里,我们将对本书其余部分所做的选择进行镜像,应用在每次运行时都会清除数据库并重置模式。
# datasource configuration
quarkus.datasource.db-kind = h2
quarkus.datasource.username = sa
quarkus.datasource.password =
quarkus.datasource.jdbc.url = jdbc:h2:file:./quarkus
# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation=drop-and-create
Listing 12-20chapter12/quarkus/src/main/resources/application.properties
所有这些都很好——也很有效——但是我们没有做任何事情来提供对使用的HibernatePostManager的任何东西的访问。为此,我们需要再添加一个扩展(管理到 JSON 的转换)和一个实际的 HTTP 端点。
我们首先需要添加resteasy-jackson扩展名:
mvn quarkus:add-extension -Dextensions="resteasy-jackson"
现在我们可以写一个非常简单的PostEndpoint。
package ch12;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;
@Path("/posts")
@Produces(MediaType.APPLICATION_JSON)
public class PostEndpoint {
@Inject
PostManager postManager;
@GET
@Transactional
public List<Post> getPosts() {
return postManager.getPosts();
}
@POST
@Transactional
public Post addPost(Post post) {
return postManager.savePost(post);
}
}
Listing 12-21chapter12/quarkus/src/main/java/ch12/PostEndpoint.java
这样,我们现在有了一个工作的/posts端点,一旦你用mvn quarkus:dev启动了应用,我们就可以通过curl与之交互:
> curl -s -w "\n" http://localhost:8080/posts
[]
> curl -s -w "\n" \
-H "Content-Type: application/json" \
-X POST \
-d'{"title":"baz","content":"bletch"}' \
http://localhost:8080/posts
{"id":1,"title":"foo","content":"bar","createdAt":"2021-07-24T23:10:45.794+00:00"}
> curl -s -w "\n" http://localhost:8080/posts
[{"id":1,"title":"foo","content":"bar","createdAt":"2021-07-24T23:10:45.794+00:00"}]
如果您修改任何一个类来添加调试输出或额外的功能(例如错误检查),Quarkus 将为您动态地重新编译并重新部署,这使得用它进行开发变得非常好。 10
摘要
我们在本章中看到的是一个非常非常粗略的关于 Hibernate 与几种不同技术集成的概述:Spring、Spring Data JPA(构建于 Spring 之上)、ActiveJ 和 Quarkus。在 Spring Boot 和夸尔库斯的例子中,Hibernate 的工具被很好地集成到了框架中,所以当你读到这篇文章时,他们可能还在 Hibernate 5 上;查看 Hibernate 6 集成状态的文档(和 Web)。
在下一章,我们将回到标准 Hibernate,看看我们如何对数据进行版本化。
十三、Hibernate Envers
Hibernate Envers ( www.google.com/url?q=https://hibernate.org/orm/envers/&sa=D&source=editors&ust=1628275087296000&usg=AOvVaw1fIkFMUR6OnHQP5ynNfRX_ )是一个项目,它提供了对实体随时间变化的样子的访问——也就是说,对实体状态进行版本控制。这意味着,如果你已经将一个实体标记为被 Envers 跟踪,或者“被审计”——通过一个相当聪明的名字@Audited注释 Hibernate 将跟踪对该实体所做的更改,并且你可以随时访问该实体。
“版本”是什么意思?
在我们跳进兔子洞之前,我们应该讨论一下版本或者“修订”的含义。
在 Envers 中,修订号实际上是针对整个数据库跟踪的数据库突变【1】的一种计数,而不是针对给定实体的更新的计数器。因此,当我们提到修订时,我们实际上是指数据库在特定时间点的快照,对于标记为由 Envers 管理的实体。
**因此,修订不一定是线性的。你没有一个单独的版本附加到每个实体上——一个Post可能按顺序附加修订版 1284、1826、19893,而不是一个更加语义化的“版本 1、2 和 3”的版本系统例如,如果我们有一个实体代表一个主键为1207的Purchase Order,它可能在三个事务中被更新:插入是一个事务,状态为TO_BE_PROCESSED。然后我们可能会再次更新它,比如说PROCESSING,然后用SHIPPED再次更新——但是没有保证更新的数量与实际的修订号之间的关系。实际上,我们甚至会在示例代码中看到这一点。
创建简单的项目
Envers 在概念上非常简单:当更改被写入事务中的一个实体时,它被“版本化”——分配一个修订——并且更新与实体本身分开存储。 2 因此,在整本书中,我们可以像我们展示的那样使用我们的实体,完全不知道 Envers,但是如果实体被标记为被审计,我们将能够跟踪应用到实体的每一个变化。
让我们看看这是如何做到的。首先,当然,我们需要我们的项目模型。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>hibernate-6-parent</artifactId>
<version>5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chapter13</artifactId>
<dependencies>
<dependency>
<groupId>com.autumncode.books.hibernate</groupId>
<artifactId>util</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
</dependencies>
</project>
Listing 13-1chapter13/pom.xml
在pom.xml中没有什么特别的——我们包含了hibernate-envers工件,它提供了@Audited注释,但是这确实是我们和其他章节项目的主要区别。
我们的hibernate.cfg.xml长得也很正常;我们引用了chapter13.model.User(我们很快就会看到),但那只是对一个实体的常规引用;我们的 Hibernate 配置也没有什么特别的。
<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url">jdbc:h2:file:./db13</property>
<property name="connection.username">sa</property>
<property name="connection.password"/>
<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<property name="use_sql_comments">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">create-drop</property>
<mapping class="chapter13.model.User"/>
</session-factory>
</hibernate-configuration>
Listing 13-2chapter13/src/main/resources/hibernate.cfg.xml
为了完整起见,我们还有一个logback.xml(存储在src/main/resources中,与hibernate.cfg.xml放在一起),它是从我们的其他章节复制来的;它和其他的logback.xml有相同的内容,所以我们将保存一棵树 3 ,不再重复。
*剩下我们的实体本身。我们将为一个用户建模,他有一组组;模型本身非常简单,一个具有一些属性的实体和一个用于组的元素集合。
package chapter13.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.envers.Audited;
import javax.persistence.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@Entity
@Data
@NoArgsConstructor
@Audited
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer id;
@Column(unique = true)
String name;
boolean active;
@ElementCollection
Set<String> groups;
String description;
public User(String name, boolean active) {
this.name = name;
this.active = active;
}
public void addGroups(String... groupSet) {
if (getGroups() == null) {
setGroups(new HashSet<>());
}
getGroups().addAll(Arrays.asList(groupSet));
}
}
Listing 13-3chapter13/src/main/java/chapter13/model/User.java
要启用审计,我们需要做的就是在类级别添加@Audited注释。这个注释的意思是,当更新(包括创建和删除)应用于实体时,它的状态应该在提交事务时作为特定修订的一部分保存;因此,我们可以在单个事务中更改name和description并获得一个修订,或者我们可以在一个事务中更新name,提交它,然后在另一个事务中更新description,并最终获得两个新的修订,每次更新一个。
我们可以像更新简单属性一样轻松地更新集合,如groups所示;对集合的更改将是修订的一部分。
正如我们在其他章节中所做的,我们将创建一些例子,从一个BaseTest抽象类开始。
这个类有一个setup()方法,它基本上创建了对一个User实体的多个修改 4 ,将实体的主键存储在一个本地数组中,这样它就可以在 lambda 中被引用。它还有两个实用方法来查找User在其历史中特定点的修订;当我们在ValidateRevisionData类中进行第二次测试时,我们将探索它们是如何工作的。
请做好准备:这有很多重复的操作!
package chapter13;
import chapter13.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.envers.AuditReader;
import org.hibernate.query.Query;
import org.testng.annotations.BeforeClass;
import javax.persistence.EntityManagerFactory;
import static org.testng.Assert.*;
import static org.testng.Assert.assertEquals;
abstract class BaseTest {
int[] userId = {0, 1};
User createUser(Session session, String username) {
User user = new User(username, true);
user.setDescription("description");
user.addGroups("group1");
session.save(user);
return user;
}
@BeforeClass
public void setup() {
SessionUtil.forceReload();
SessionUtil.doWithSession(session -> {
Query<User> deleteQuery = session.
createQuery("delete from User u");
deleteQuery.executeUpdate();
});
SessionUtil.doWithSession((session) -> {
User user = createUser(session, "user1");
userId[0] = user.getId();
});
SessionUtil.doWithSession(session -> {
User user = createUser(session, "user2");
userId[1] = user.getId();
});
SessionUtil.doWithSession((session) -> {
User user = session.byId(User.class).load(userId[0]);
assertTrue(user.isActive());
assertEquals(user.getDescription(),
"description");
});
SessionUtil.doWithSession((session) -> {
User user = session.byId(User.class).load(userId[0]);
user.addGroups("group2");
user.setDescription("1description");
});
SessionUtil.doWithSession((session) -> {
User user = session.byId(User.class).load(userId[1]);
user.addGroups("group2");
user.setDescription("2description");
});
SessionUtil.doWithSession((session) -> {
User user = session.byId(User.class).load(userId[0]);
user.setActive(false);
});
SessionUtil.doWithSession((session) -> {
User user = session.byId(User.class).load(userId[0]);
assertFalse(user.isActive());
assertEquals(user.getDescription(), "1description");
});
}
User findUserAtRevision(
AuditReader reader,
Number revision) {
return findUserAtRevision(
reader,
userId[0],
revision
);
}
User findUserAtRevision(
AuditReader reader,
int pk,
Number revision) {
reader.find(User.class, pk, revision);
return reader.find(
User.class,
"chapter13.model.User",
pk,
revision
);
}
}
Listing 13-4chapter13/src/test/java/chapter13/BaseTest.java
这是一个很长的类,但是它真的(真的)简单:长度取决于它需要使用单独的事务来按顺序进行大量更新。它只创建多个用户,并在多个事务中更新它们;它还保存生成的主键供以后使用。
表 13-1
BaseTest中的用户状态
id
|
name
|
description
|
groups
|
active
|
revision
|
| --- | --- | --- | --- | --- | --- |
| 1 | user1 | description | group1 | true | 1 |
| 2 | user2 | description | group1 | true | 2 |
| 1 | user1 | 1description | group1,group2 | true | 3 |
| 2 | user2 | 2description | group1,group2 | true | 4 |
| 1 | user1 | 1description | group1,group2 | false | 5 |
注意修订,是而不是对* 实体的修订*计数器。如果它们是,第三行的修订将是2,但它不是;这实际上使我们能够按日期获得整个数据库的快照,如果我们想要的话,这是非常强大的。这种力量的成本在于,修订版并不像原本那样容易解释;我们不再按实体计数更新,而是按数据库事务计数。
我们用来与修订交互的主要接口是AuditReader,它是从一个AuditReaderFactory中获得的。我们的基本使用模式是通过AuditReader.getRevisions()获取可用的修订,然后使用AuditReader.find()或AuditReader.getQuery()加载特定的修订。
有一种方法可以在某个时间点应用修订时获取修订(同样,也有一种方法可以获取特定修订的日期)。这些机制可能比getRevisions()更有用,但是需要构建一个相当慢的测试工具。还有一个AuditQuery界面,我们将很快看到它的实际应用。
让我们来看一个简单的验证,验证版本是否按照我们的预期存储。我们将创建一个ValidateRevisions测试,从我们的BaseTest扩展而来,这样它就可以访问一个存储了修订的User。
您从一个AuditReaderFactory获得一个AuditReader,传入一个Session用于数据库访问,如下所示:
AuditReader reader = AuditReaderFactory.get(session);
因此,我们需要确保我们在Session的上下文中做所有的事情。第一个测试只是验证我们有对一个User的修改——它的标识符在userId[0]中,存储在一个数组中,所以我们可以在 lambda 中使用它。我们通过使用AuditReader. getRevisions(),传入实体类型(User.class)和该类型的主键来获得修订。
对于来自我们的BaseTest的数据,获得 id 为1的User的修订应该会给我们一个1,3,5的列表。如果我们没有三个修订——这是由BaseTest.setup()方法设置的——那么一定是哪里出错了,但是我们不一定能测试出特定的修订号,因为修订号指的是拍摄快照的时间,而不是实体的更新。 5 这已经重复了几次,但是修订号很像是一个事务的计数器,而不是对特定实体的更新。
package chapter13;
import chapter13.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.*;
public class ValidateRevisionCountTest extends BaseTest {
@Test
public void validateRevisionCount() {
SessionUtil.doWithSession((session) -> {
AuditReader reader = AuditReaderFactory.get(session);
List<Number> revisions =
reader.getRevisions(User.class, userId[0]);
assertEquals(revisions.size(), 3);
});
}
}
Listing 13-5chapter13/src/test/java/chapter13/ValidateRevisionCountTest.java
现在是我们验证这些修订包含什么的时候了。让我们创建另一个测试,即ValidateRevisionData测试。
package chapter13;
import chapter13.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.testng.annotations.Test;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
public class ValidateRevisionDataTest extends BaseTest {
@Test
public void testUserData() {
SessionUtil.doWithSession((session) -> {
AuditReader reader = AuditReaderFactory.get(session);
List<Integer> revisions =
reader.getRevisions(User.class, userId[0])
.stream()
.map(Number::intValue)
.collect(Collectors.toList());
List<User> userRevs =
revisions
.stream()
.map(rev -> findUserAtRevision(reader, rev))
.collect(Collectors.toList());
// first revision
assertEquals(
userRevs.get(0).getDescription(),
"description"
);
assertEquals(
userRevs.get(0).getGroups(),
Set.of("group1")
);
// second revision
assertEquals(
userRevs.get(1).getDescription(),
"1description");
assertEquals(
userRevs.get(1).getGroups(),
Set.of("group1", "group2")
);
// third, and last, revision
assertFalse(
userRevs.get(2).isActive()
);
assertEquals(
session.load(User.class, userId[0]),
userRevs.get(2)
);
System.out.println(reader.getRevisionDate(2));
System.out.println(reader.getRevisionDate(1));
});
}
}
Listing 13-6chapter13/src/test/java/chapter13/ValidateRevisionDataTest.java
这个类做得更多,但是它仍然很简单。它做的第一件事是获取修订,就像ValidateRevisionCount做的那样,但是然后它将这些修订映射到一个User对象列表中——这将对应于User实体的完整历史。班上的其他人只是简单地验证每一次修订都有我们期望的变化。
第一组断言(使用userRevs.get(0))验证User的初始状态,它有一个简单的描述(“第一个描述”)和一个组(“group1”)。
第二组断言检查组和描述的更新,从创建User后的第一次更新开始。
第三组断言验证了active标志已经正确更改——然后我们将第三次修订与User的当前状态进行比较,如Session.load()所示,以证明随着时间的推移AuditReader实际上正在返回有效的实体表示。
findUserAtRevision()方法的工作方式是利用AuditReader.find()方法。这种方法有很多变体;以下是一些例子:
<T> T find(Class<T> cls, Object primaryKey, Number revision)
<T> T find(Class<T> cls, Object primaryKey, Date date)
<T> T find(Class<T> cls, String entityName, Object key, Number revision)
<T> T find(Class<T> cls, String entityName, Object primaryKey,
Number revision, boolean includeDeletions)
我们利用其中的第三个,主要是因为它给了我们一个查看参考文献entityName的机会,这是一个小的误导。在 JPQL 中,我们的User类的“实体名”是"User",就像在from User u中一样——但是在这里,它实际上是完全限定的实体名,所以我们需要传递chapter13.model.User,因为这是 Envers 用来查找被审计实体的。
一旦我们理解了这个小障碍,类型就相当清楚了:引用的具体内容Class、完全限定的实体名、实体的主键(在本例中是通过userId[0])以及传入的修订号。
我们也可以利用修订的快照性质来获取数据。再来看看另一个测试,ValidateRevisionSnapshot。
package chapter13;
import chapter13.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.testng.annotations.Test;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.testng.Assert.*;
public class ValidateRevisionSnapshotTest extends BaseTest {
@Test
public void testUserData() {
SessionUtil.doWithSession((session) -> {
AuditReader reader = AuditReaderFactory.get(session);
List<Integer> revisions =
reader.getRevisions(User.class, userId[0])
.stream()
.map(Number::intValue)
.collect(Collectors.toList());
int indexOfLastRevision = revisions.size() - 1;
int lastRevision = revisions.get(indexOfLastRevision);
User lastUser = findUserAtRevision(reader, lastRevision);
User prevUser = findUserAtRevision(reader, lastRevision - 1);
assertTrue(lastRevision - 1 > revisions.get(indexOfLastRevision - 1));
assertNotEquals(lastUser.isActive(), prevUser.isActive());
});
}
}
Listing 13-7chapter13/src/test/java/chapter13/ValidateRevisionSnapshotTest.java
这个类看起来比实际复杂得多。
首先,它获得修订列表——就像我们的ValidateRevisionData一样。
然后,它通过计算修订在列表中的位置来获得“最后的修订”;我们应该有修订版 1、3 和 5。(实际上,我们将测试这一点,因为否则我们根本不会演示任何东西。)
然后它调用我们的findUserAtRevision()方法,使用修订版5——当前修订版——和修订版4,这是数据库的前一个快照。修订版 4 的User应该与修订版 3 的User相同——毕竟,修订版 4 更新了我们的另一个用户,而不是这个——我们可以通过查看isActive()状态来测试。如果我们关于修订版的断言是正确的,那么User的修订版4应该被设置为活动的,而修订版 5 不应该。 6
我们的BaseTest运行得相当快,否则我们可以使用AuditReader中的一些信息方法来捕获给定日期(或给定修订的编写时间)的修订号:
AuditReader reader=AuditReaderFactory.get(session);
Date revisionDate=reader.getRevisionDate(4);
// or
Date date=somePointInThePast();
Integer revisionNumber=reader
.getRevisionNumberForDate(date)
.intValue();
当然,有不同的种修订。在这里,我们已经更新了User三次。但是,我们也可以删除用户;接下来会发生什么?是时候找出答案了。
我们将创建一个测试,删除了用户在我们的BaseTest中精心创建的,这将在该测试的上下文中创建一个第六修订。我们实际上将在这个类中有多个测试:一个验证第四个修订的创建,其他的检查当您使用AuditReader.find()查询修订时会发生什么。
package chapter13;
import chapter13.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.List;
import static org.testng.Assert.*;
public class HandleDeletedRevisionsTest extends BaseTest {
@BeforeClass
void deleteUser() {
SessionUtil.doWithSession(session -> {
User user = session.load(User.class, userId[0]);
session.delete(user);
});
}
@Test
public void countRevisions() {
SessionUtil.doWithSession(session -> {
AuditReader reader = AuditReaderFactory.get(session);
List<Number> revisions =
reader.getRevisions(User.class, userId[0]);
assertEquals(revisions.size(), 4);
});
}
@Test
public void findRevisionNoDeleted() {
User user = runQueryForVersion(false);
assertNull(user);
}
@Test
public void findRevisionDeleted() {
User user = runQueryForVersion(true);
assertNotNull(user);
assertNull(user.getName());
assertNull(user.getDescription());
}
private User runQueryForVersion(
boolean includeDeleted
) {
return SessionUtil.returnFromSession(session -> {
AuditReader reader = AuditReaderFactory.get(session);
User user = reader.find(
User.class,
"chapter13.model.User",
userId[0],
6,
includeDeleted
);
return user;
});
}
}
Listing 13-8chapter13/src/test/java/chapter13/HandleDeletedRevisionsTest.java
对于includeDeletions参数,findRevisionNoDeleted()测试通过false;在这种情况下,所发生的是find()返回null,因为在一个被删除的修订中,实际上没有用户。
然而,在某些情况下,您可能想要捕获关于实际被删除条目的数据(即,您正在捕获被删除的事件)。对于这种情况,您可以为includeDeletions传递true,在这种情况下,您将得到一个实际的实体,一个User。然而,User人口众多;您将获得简单属性的null或默认值。 7
寻找特定数据的修订
正如我们已经展示的,AuditReader接口提供了find(),但它也提供了创建相当流畅的AuditQuery的能力,这除了在很大程度上是无类型的,需要我们对结果进行造型之外,还是相当有用的。 8
在我们的User转换中,User开始被标记为active,最后一次更新将用户标记为不活动。我们实际上可以通过向AuditQuery添加属性来找到User处于活动状态的最后一个版本,如FindLastActiveUserRevisionTest类所示。
package chapter13;
import chapter13.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.AuditQuery;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
public class FindLastActiveUserRevisionTest extends BaseTest {
@Test
public void findLastActiveUserRevision() {
SessionUtil.doWithSession((session) -> {
User user = getUserWhenActive(session);
System.out.println(user);
assertEquals(user.getDescription(), "1description");
});
}
protected User getUserWhenActive(Session session) {
AuditReader reader = AuditReaderFactory.get(session);
AuditQuery query = reader.createQuery()
.forRevisionsOfEntity(User.class, true, false)
.addOrder(AuditEntity.revisionNumber().desc())
.setMaxResults(1)
.add(AuditEntity.id().eq(userId[0]))
.add(AuditEntity.property("active").eq(true));
User user = (User) query.getSingleResult();
return user;
}
}
Listing 13-9chapter13/src/test/java/chapter13/FindLastActiveUserRevisionTest.java
这里,我们有一个getUserWhenActive()方法,该方法构建一个查询来查找最近设置为active的User。
我们先用forRevisionsOfEntity(User.class, true, false)告诉查询我们要找什么类型的实体。这里的第一个布尔值是“选择的实体”,这意味着我们将得到实际的User实体,而不是关于修订本身的信息;第二个布尔值用于选择删除的实体,我们对此不感兴趣。 9
接下来,我们向结果添加一个顺序;我们希望结果按照修订号的降序排列(即最新的排在最前面)。
我们只对单个结果感兴趣,所以我们使用setMaxResults(1)。
然后,我们通过一个简单的add(AuditEntity.id().eq())调用向查询添加一个id——我们基本上是告诉查询向搜索添加一个谓词。我们在AuditEntity.property("active")上添加了另一个谓词,这样我们就可以寻找一个具有特定主键和特定属性值的实体。
之后,就是简单本身;我们运行查询并期待结果;我们检查以确保description与我们从测试数据中期望的修订相匹配,瞧!
还原数据的示例
Envers 没有提供一种机制,我们可以通过这种机制轻松地将数据倒回先前已知的状态;我们不能告诉 Envers 我们希望版本 2 成为“活动”版本。然而,我们可以访问修订版 2,并将实体的当前状态设置为与之前的状态相匹配。
让我们重新访问FindLastActiveUserRevisionTest并扩展它,将标记为不活动的User(并带有新的描述)恢复到先前的状态。
package chapter13;
import chapter13.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
public class RevertDataTest extends
FindLastActiveUserRevisionTest {
@Test
public void revertUserData() {
SessionUtil.doWithSession((session) -> {
User auditUser = getUserWhenActive(session);
assertEquals(auditUser.getDescription(), "1description");
// now we copy the audit data into the "current user."
User user = session.load(User.class, userId[0]);
assertFalse(user.isActive());
user.setActive(auditUser.isActive());
user.setDescription(auditUser.getDescription());
user.setGroups(auditUser.getGroups());
});
// let's make sure the "current user" looks like what we expect
SessionUtil.doWithSession((session) -> {
User user = session.load(User.class, userId[0]);
assertTrue(user.isActive());
assertEquals(user.getDescription(), "1description");
});
}
}
Listing 13-10chapter13/src/test/java/chapter13/RevertDataTest.java
这实际上很简单:首先,我们重用来自FindLastActiveUserTest类的getActiveUser();这是我们更新的源数据。
然后我们从数据库中加载User实体(“当前修订”)。我们从测试数据中知道active标志应该是false,但是我们还是在这里检查它。 10
在那之后,我们有一个被管理的User引用,因为我们刚刚加载了它;我们将从getActiveUser()加载的修订版中的数据复制到托管的User引用中。当Session结束并且事务被提交时,Session将创建一个新的修订(使用我们刚刚设置的数据)并将其写入数据库。测试的最后一部分重新加载用户并验证我们刚刚编写的更新。
摘要
Envers 是一个非常简单的 Hibernate 实体版本管理库。它不太可能适合每一个需求,但是它的确具有非常灵活的查询能力,并且可以通过向实体类添加一个简单的注释来满足大多数审计需求。
Hibernate 是 Java 中为关系系统提供持久化的最流行的机制之一。我们已经展示了适用于大多数应用的特性,包括基本的持久化操作(创建、读取、更新、删除)、对象类型之间的关联,以及提供和使用审计数据。
我们还看到了许多正在使用的“更好的实践”11——重点是测试和构建工具(分别通过 TestNG 和 Maven),我们还看到了如何使用现代 Java 特性来简化我们的一些代码(特别是在我们后面的章节中使用 lambdas 来隐藏事务管理)。
我们希望你已经学到了一些有趣的东西,尤其是你读过的相关信息;我们也希望你喜欢这本书。
***

浙公网安备 33010602011771号