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 编写的。我们已经掩饰了一些实现细节,但是我们将在第二章中深入讨论这些细节。
Footnotes 1关系数据库是一个数据集合,每个数据都经过正式描述并组织成“行”——与给定事物相关的数据,如人、产品、消息——和列,如人的名字、姓氏、产品号、产品描述等等。还可以制定规则,将行描述为彼此相关,因此订单可以描述为“拥有”行项目,并且行项目的产品编号必须存在。还有其他数据库类型,但关系数据库可能是最常见的数据库类型。
2
Jakarta EE 是一组规范,允许 Java 社区使用“企业级”规范来完成常见任务。这包括为万维网构建应用、与数据库对话(包括直接 SQL 或通过 Java Persistence API,或“JPA”),或使用消息队列,等等。它过去被称为“Java EE”(在此之前是“J2EE”),但在 2019 年甲骨文将所有权交给了社区,出于法律原因,它被重新命名。
3
还要注意的是,Hibernate 发布后,出现了许多替代方案,其中许多解决了 Hibernate 看起来不太理想的地方。它们大多是不同的范例,通常你会发现 Hibernate 做了一件出色的工作,成为关系数据库和 Java 对象模型之间的媒介,而替代方案倾向于让你从数据模型的角度考虑你的 Java 对象。
4
Maven 叫“正面”,在 11 次抛硬币中,Maven 赢了 6 次。
5
如果您下载源代码,您会在 XML 中看到一些非常有趣的注释。这些是asciidoctor标签,它们在这里用来隐藏不相关的信息;这个想法是,如果你真的输入了这段代码,它将会是你真正需要的,而不是显示不相关的信息。
6
它实际上是 Java 的当前 LTS 版本——LTS 意味着它得到了长期的支持。Java 的实际“当前”版本是 Java 16,Java 17——JVM 的下一个 LTS 版本——即将到来,但 16 不是 LTS 版本,17 在起草时还没有发布。
7
如果您正在使用下载的源代码,请注意 H2 的当前版本可能比本参考文献更新。当然,这正是为什么首先设置该属性,以便更容易跟踪当前版本。
8
这实际上影响了本章的起草;审查者实际上发现了一个错误,你的作者没有正确地完成映射,因为它是手动完成的。
9
Hibernate 代码比 JDBC 代码“差”的唯一标准是“不需要任何 Hibernate 知识”你得对 Hibernate 有所了解才能使用 Hibernate,这是给定的;如果你没有任何 Hibernate 知识,但是你有JDBC 知识,那么相比之下 Hibernate 版本必然显得很陌生。
10
公平地说,我们也可以用MessageEntity来做这件事,但是这是一个一次性的类,在 JDBC 测试中,它需要用id来填充。
11
如果使用注释来配置映射,那么两个注释@Entity和@Id是必需的。如果你不使用注释,你至少需要使用它们的等价物——等价物在本书中不会深入讨论。
12
我们实际上将在第三章中创建一个实用模块,并用它来隐藏构建SessionFactory的过程。如果我们使用像 Jakarta EE 或 Spring 这样的应用框架,他们会有自己初始化SessionFactory的方式,这样重复、冗长的代码就会被隐藏起来。
**
二、集成和配置 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 的更多核心概念。
Footnotes 1这意味着如果你使用的是旧版本的 JVM,你必须坚持旧版本的 Hibernate。这应该不会太麻烦;自 2015 年 4 月起,Java 7 已经寿终正寝。如果您还没有升级,现在是时候升级了,这是出于安全考虑,当然还有 Java 8 很好的事实。
2
请注意,直接从 servlet 等调用 Hibernate 或任何存储机制通常不是很明智。我们将在第 11 和 12 章中看到一些更好的策略,但为了简洁起见,这样做效率很低。
3
这本书的第三版实际上使用了 HSQLDB,第四版我们换成了 H2。这种转变没有具体的原因,但社区中的研究表明,由于各种原因(主要集中在《H2》是由 HSQL 的原作者编写的),H2 比 HSQLDB 更受青睐。你的作者倾听人民的心声。
4
另外值得注意的是,有 Maven 的插件可以像 Maria db(MySQL 的变种)一样嵌入外部数据库;如果你有兴趣的话,可以在 https://github.com/vorburger/MariaDB4j 看到 MariaDB4J。但是对于我们的目的来说,H2 更小更快,而且由于 Hibernate 是独立于数据库的,所以您使用的实际数据库在很大程度上应该是不相关的。
5
“也许”在这里用得颇具讽刺意味。使用像 Maven 或 Gradle 这样的构建工具。其他的选择也是可行的,就像当一座吊桥已经为你建造好了的时候,建造一座索桥来跨越鸿沟是完全没问题的。
6
关于“哪种构建工具最好”的争论很像关于 IDEA、Emacs、NetBeans、Eclipse 和其他工具的相对优点的争论。每个人都有自己的观点,这个观点对持有它的人来说是完全正确的,只要理解是 SBT 是最坏的;然而,Maven 通常被认为不是“最好的构建工具”,就像 Eclipse 不是“最好的编辑器”一样他们很受欢迎。他们很普通。差不多了。
7
如果您不使用构建工具,请参阅您的 IDE 向项目添加库的说明。然而,值得注意的是,使用构建工具是明智的;这意味着你的构建很容易被复制。例如,如果你想向其他人展示你的代码,在没有构建工具的情况下,你必须确保他们的环境与你的环境相匹配;但是有了构建工具,你所要做的就是确保他们已经安装了工具。你可以在这本书里看到这一点;我用 Maven 描述构建,读者可以使用他们喜欢的任何编辑器或 IDE,而不会影响内容。
8
TestNG ( https://testng.org/ )是一个单元测试框架。这是 JUnit ( https://junit.org/junit5 )的一个流行替代。也喜欢 JUnit 5 可能是完全可以接受的,它有许多受 TestNG 启发的特性和一些自己的好特性。
9
就长期支持而言,目前“支持”的 Java 版本是 Java 11,这一点已经被提到至少三次了。然而,你仍然可以在 Hibernate 中使用 Java 8。
10
树中还有其他的类,但我们在这一章中不再关心 JDBC;他们在这里是因为你被承诺第二章的树和第一章的树是一样的。所有 JDBC 的东西都将被忽略。
11
使用数据库连接池也是明智的。Hibernate 有一个内置的,但它并不意味着用于生产;我们将在下一节看到更强大的数据库连接池的使用,Hibernate 使它们的使用变得简单。
12
“图式”是“图式”的复数形式 www.merriam-webster.com/dictionary/schema见。
13
详见 https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#database 。
14
参见 www.ibm.com/developerworks/library/j-jndi/?ca=dnt-62 中详细讨论这一概念的文章,尽管实现细节有些过时。
三、构建简单的应用
在这一章中,我们将创建一个应用的外壳,这将允许我们演示一些使用 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 的应用的生命周期。
Footnotes 1用@Entity标记一个实体感觉几乎是合乎逻辑的。
2
Hibernate 5 中增加了在查询中返回类型的功能,这一点很受欢迎。
3
返回像null这样的信号值是这个代码的一种风格选择。有很多方法可以在这种查询中抛出异常。我们只是在这里使用一个信号值*。*
* 4
我们将在本章的后面看到createData方法。
5
我们可以让数据库生成一组数字的平均值,而不是让 Java 流为我们计算它,这也相当容易。然而,在数据库中执行可以避免将数据从数据库转移到我们的代码中,这样可以节省大量时间,并且可能减少网络流量。一般来说,用数据库做更好,正如我们将看到的,这也很容易。
6
它通常通过使用代理对象来实现这一点。当您更改对象中的数据值时,代理会记录该更改,以便事务知道在事务提交时将数据写入数据库。如果这听起来像是魔术,它不是——但做起来也不简单。感谢 Hibernate 的作者。
7
我们将在下一章更详细地讨论托管对象这个话题。
8
在下一章中,我们将会反复讨论事务。
9
请注意,Hibernate 具有验证功能,使得数据验证变得非常容易;文本描述手动验证的方式相当乏味。
10
参见 www.h2database.com/html/advanced.html#transaction_isolation 以获取 H2 的事务凭证为例。其他数据库也有类似的文档。
11
除了我们在本书中看到的测试之外,使用create-drop做任何事情都是不可取的。这里由SessionUtil公开的功能在这里和极少数其他地方是有用的。
12
注意 Spring 有自己的方式来管理 Hibernate Session引用;在 Spring 应用中使用它是不明智的。
13
字符串混淆的解决方法是…不愉快的。我们可以用像PersonName和SkillName这样的类型来污染我们的类型系统,这样我们在引用 J. C .时就必须传入一个PersonName,但是这往往会让理论家们非常高兴——因为不可能偶然传入一个SkillName——但是这让那些不得不用 API 编程的人相当沮丧,因为他们最终不得不使用所有这些语义相似但对产品贡献很少实际值的类型。它“更加类型安全”,但也是日复一日使用的负担。
14
我们向我们的RankingService添加了很多方法,除非你修改了RankingService接口,否则一些代码还不能编译,因为我们使用了@Override来确保方法签名匹配。我们将在几个部分中包括完整的RankingService源代码,以及完整完整的 HibernateRankingService源代码,并且那些完整的清单将在本章编写时被成功编译,我保证。
15
这不是一个“真正的应用”,尽管它可能是;然而,这里实现它仅仅是为了作为一个粗略的例子。
16
检查副作用的一种可能是清除整个数据集(正如我们在 RankingTest 代码中所做的那样,通过关闭每个测试的 SessionFactory),然后清除我们期望写入的数据,并查找任何无关的数据。当然还有其他的可能性,但是所有这些都超出了我们的讨论范围。
17
几乎所有对 SQL 有一定了解的人都可能对我们如何从数据库中提取数据感到愤怒。没关系——这里的做法很差劲。它简单易懂,达到了解释的目的……但是从 CPU 的角度来看,它的效率不是很高。
18
即使有了嵌入式数据库,它也可以更快;嵌入式数据库可以使用我们的应用代码通常无法访问的数据的内部访问,甚至在我们考虑通过使用索引进行高效查询的可能性之前。
*