Hibernate6 入门手册(六)
十二、集成 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,看看我们如何对数据进行版本化。
Footnotes 1嗯,有些人可能会像我们在第十一章中所做的那样直接编写应用到 servlet API,但是他们在他们的应用方面倾注了大量的精力,这些应用有更好的选择。
2
像“功能测试”和“单元测试”这样的术语没有绝对的定义。一些程序员会拒绝这些定义。有些不会。在任一组都没问题;最重要的事情是把事情做好,而不是争论事情的定义有多清晰,至少在这个案例中是这样的。
3
如果你想知道,Post是由 IDEA 自动生成的。其他 ide 可以做同样的事情,代码看起来也很相似;就本章的目的而言,这已经“足够好”了。
4
使用Date而不是OffsetDateTime绝对是一种捷径,主要是为了防止出现更长的程序清单和额外的依赖项,它们并没有真正向我们展示任何新的东西。
5
如果你还没有看过这类操作的例子,可以看看这本书的其余部分!这是本好书,你会喜欢的。这个作家很搞笑。
6
撰写本文时,Hibernate 6 的 Spring 数据还没有更新。对 Hibernate 6 进行更新后,代码在功能上看起来和这里一样,尽管可能会有一些小的变化。
7
“它不太依赖传统的 Java 架构模式”是一种啰嗦的说法,它在重用一些相同的术语和概念的同时,很大程度上忽略了 Jakarta EE。
8
jsonpp见https://jmhodges.github.io/jsonpp/;你也可以替换成json_pp,这取决于你的愿望和你安装的东西。
9
要使用 Quarkus 构建原生映像,您需要安装 GraalVM 和原生映像工具;详情和教程见 https://quarkus.io/guides/building-native-image 。
10
公平地说,Spring Boot 也提供了热重装功能,但它不像夸尔库斯热重装设施那样完整。
十三、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 来隐藏事务管理)。
我们希望你已经学到了一些有趣的东西,尤其是你读过的相关信息;我们也希望你喜欢这本书。
Footnotes 1数据库突变包括将实体插入数据库表、更新或删除的事件。
2
因此,Envers 修订版是指在给定事务中应用于被审计实体的更新。
3
或者我们会保存一棵树的电子版本,如果你是在电子书上而不是在纸上阅读的话。
4
我们的setup()也调用SessionUtil.getInstance().forceReload(),这导致整个数据库重新初始化到一个已知的状态;如果我们不这样做,我们的测试就会互相干扰。
5
例如,如果您将另一个用户添加到 BaseTest 中,修订号将会改变……但是为特定实体传回的修订号将是相同的,除非您也为该实体添加了更新。
6
所有这些解释都假设修订如表 13-1 所示进行;这是没有任何保证的,这就是为什么我们不能把修订号作为绝对具体的参考。我们的BaseTest每次测试都会强制一个新的数据库,所以这里的应该是一致的,但是没有绝对的保证。
7
奇怪的是,被删除的User引用仍然填充了它的groups集合。恩弗斯有时会…很奇怪。
8
看到了吗,编辑先生?我知道如何使用讽刺吗?我对此相当肯定。
9
我们对删除的实体不感兴趣,因为,首先,它们是删除的,我们不想要它们;第二,我们实际上是在寻找用户的最新版本,这样他们就被标记为活动。被删除的User根据定义是不活动的(active标志被设置为假)。
10
你应该永远相信你的作者——我们为什么要骗你?–但是值得信赖的标志之一是永远不要让你的读者信任你。我宁愿给你看。
11
我想说“最佳实践”,但这听起来相当自私。
***