Hibernate 6 中文文档(一)(版本6.3.1.Final)

译自:An Introduction to Hibernate 6

文中相关链接需要科学上网方可访问,后续有时间再逐个翻译。文章中如果存在任何不准确的地方,欢迎指正。

尚未完成,不断更新中....

 系列文章:

Hibernate 6 中文文档(一)

Hibernate 6 中文文档(二)

Hibernate 6 中文文档(三)

Hibernate 6 中文文档(四)

Hibernate 6 中文文档(五)

目录

前言

1. 介绍

1.1. Hibernate 和 JPA

1.2. 使用 Hibernate 编写Java代码

1.3.  Hibernate 快速上手

1.4. JPA 快速上手

1.5. 编写持久化模块的代码

1.6. 测试持久化逻辑

1.7. 架构与持久化层

1.8. 概览


前言

Hibernate 6是世界上最受欢迎且功能丰富的对象关系映射(ORM)解决方案的一次重大改版。这次改版几乎触及了Hibernate的每个子系统,包括API、映射注解和查询语言。这个新版本的Hibernate更加强大、更加健壮,而且类型安全性更强。

在这么多改进中,要总结这项工作的重要性非常困难。但以下几个主题最为突出

Hibernate 6:

  • 充分利用了过去十年来关系数据库的进展,更新了查询语言以支持现代SQL方言中的众多新构造
  • 在不同数据库间表现出更加一致的行为,极大地提高了可移植性,并且从独立于方言的代码中生成更高质量的DDL(数据定义语言)
  • 在访问数据库之前更加严格地验证查询,改善了错误报告
  • 提高了对象关系映射注解的类型安全性,明确了API、SPI和内部实现的分离,并修复了一些长期存在的架构缺陷
  • 移除或废弃了旧版API,为未来的演进奠定了基础
  • 更好地使用了Javadoc,为开发人员提供了更多信息

Hibernate 6 和 Hibernate Reactive 现在是Quarkus 3的核心组件,Quarkus 3是Java中最令人激动的云原生开发新环境,而Hibernate依然是几乎每个主要Java框架或服务器的首选持久化解决方案。

不幸的是,Hibernate 6的变化使得大部分现有的关于Hibernate的信息在书籍、博客文章和stackoverflow上都已经过时。

本指南是关于当前特性集和推荐用法的最新、高层次的讨论。它不试图覆盖每个特性,应该与其他文档一起使用:

Hibernate用户指南详细讨论了Hibernate的大部分方面。但由于要涵盖的信息太多,难以实现可读性,因此它最适合作为参考。在必要时,我们将提供到用户指南相关章节的链接。

1. 介绍

Hibernate通常被视为一个库,它可以被用来轻松地将Java类映射到关系数据库表。但这种看法并没有充分体现关系数据本身的核心作用。因此,关于它的功能更准确的描述应该是:

Hibernate将关系数据以一种自然且类型安全的形式展现给Java程序,使得编写复杂查询和处理查询结果变得容易,让程序能够轻松地将内存中所做的更改与数据库同步,遵循事务的ACID属性,并且在基本持久化逻辑编写后,允许进行性能优化。

这里关注的是关系数据,以及类型安全的重要性。对象/关系映射(ORM)的目标是消除脆弱且不安全的代码,使得长期来看,更容易维护大型程序。

ORM通过解放开发人员手工编写冗长、重复和脆弱的代码,将对象图形转换为数据库表格,以及从扁平的SQL查询结果集重建对象图形的需求,从而减轻了持久性方面的痛点。更妙的是,在基本持久性逻辑编写后,ORM使性能调优变得更加容易。

一个常见的问题是:我应该使用ORM,还是纯SQL?答案通常是:两者都用。JPA和Hibernate是为与手写SQL配合使用而设计的。大多数SQL查询逻辑较好的程序都会受益于ORM的帮助。但是,如果Hibernate在某个特定的SQL查询中上,使查询本身变得更加困难,使用更适合该问题的解决方案才是明智的选择,不需要固执的使用Hibernate!你使用了Hibernate解决持久性方面的问题,并不意味着你必须在所有地方都使用它。

开发人员经常问有关Hibernate和JPA之间关系的问题,因此,首先让我们先来简单了解一下它们的发展史。

1.1. Hibernate 和 JPA

Hibernate 是 Java Persistence API(现在是Jakarta)或JPA背后的灵感来源,并包含了对该规范最新修订版的完整实现。

译者注:

  •  例如,旧版本(Java 8以及之前)中,我们通过如下方式引入:
import javax.persistence.*;
  • 现在新版本中(自 Java 9 开始)我们可以使用:
import jakarta.persistence.*;

Hibernate和JPA的早期历史

Hibernate项目始于2001年,当时Gavin King对EJB 2中的Entity Beans感到非常沮丧。它很快超越其他开源和商业竞争对手,成为Java中最流行的持久化解决方案,并且与Christian Bauer合著的《Hibernate in Action》成为了一本具有影响力的畅销书。

在2004年,Gavin和Christian加入了一个名为JBoss的小型初创公司,其他早期的Hibernate贡献者也很快加入进来:Max Rydahl Andersen,Emmanuel Bernard,Steve Ebersole和Sanne Grinovero。

不久后,Gavin加入了EJB 3专家组,并说服该组废弃Entity Beans,转而采用模仿Hibernate的全新持久性API。后来,TopLink团队的成员也参与其中,Java持久性API成为Sun、JBoss、Oracle和Sybase等主要公司,尤其是在Linda Demichiel的领导下,进行合作演进的产物。

在接下来的20年中,许多才华横溢的人为Hibernate的发展做出了贡献。我们特别感谢Steve,多年来一直领导着该项目,因为Gavin开始专注于其他工作。

译者注: EJB 2(Enterprise JavaBeans 2)是Java EE(Enterprise Edition)规范中的一个版本,它定义了一种用于构建企业级Java应用程序的组件模型。EJB 2 最初发布于1999年,是EJB 1 的改进版本,它提供了一种分布式计算模型,允许开发者构建分布式、可伸缩和安全的应用程序。

在API方面,我们可以将 Hibernate 的API分为三个基本元素:

  • 一组实现了JPA定义API的类,最重要的是EntityManagerFactory和EntityManager接口,以及JPA定义的O/R映射注解。
  • 一个本地API,公开了所有可用功能的集合,主要围绕着SessionFactory接口(它扩展了EntityManagerFactory)和Session接口(它扩展了EntityManager)。
  • 一组映射注解,这些注解扩充了JPA定义的O/R映射注解,并且可以与JPA定义的接口或本地API一起使用。

Hibernate还为扩展或与Hibernate集成的框架和库提供了一系列SPI(服务提供接口),但我们在这里不关心这些内容。

作为应用程序开发人员,你必须决定是否:

  • 使用 Session 和 SessionFactory 编写程序,或
  • 在合理的范围内使用 EntityManage r和 EntityManagerFactory 编写代码,以最大程度地提高到其他JPA实现的可移植性,在必要时才回退到本地API。

无论选择哪种路径,你大部分时间都将使用JPA定义的映射注解,并且在处理更高级的映射问题时使用Hibernate定义的注解。

你可能会想知道是否可能仅使用JPA定义的API来开发应用程序,实际上是可能的。JPA是一个非常好的基准,它真正解决了对象/关系映射问题的基本要点。但是,如果没有本地API和扩展映射注解,你将错过Hibernate许多强大的功能。

由于Hibernate存在于JPA之前,并且JPA是以Hibernate为蓝本设计的,我们不幸地在标准API和本地API之间存在一些竞争和名称上的重复。例如:

HibernateJPA
org.hibernate.annotations.CascadeTypejavax.persistence.CascadeType
org.hibernate.FlushModejavax.persistence.FlushModeType
org.hibernate.annotations.FetchModejavax.persistence.FetchType
org.hibernate.query.Queryjavax.persistence.Query
org.hibernate.Cachejavax.persistence.Cache
@org.hibernate.annotations.NamedQuery@javax.persistence.NamedQuery
@org.hibernate.annotations.Cache@javax.persistence.Cacheable

通常,Hibernate本地API提供了JPA中缺少的一些额外功能,因此这并不算是一个缺点。但这是需要注意的地方。

1.2. 使用 Hibernate 编写Java代码

如果您完全不了解 Hibernate 和 JPA,您可能会好奇,持久化相关功能的代码的层次结构是如何划分的。

通常,我们的持久化相关代码分为两个层次:

  1. 一个用Java表示的数据模型,通常以一组带有注解的实体类的形式存在。
  2. 一大堆与Hibernate的API交互的函数,用于执行与各种事务相关的持久化操作。

第一点中提到的数据或“领域”模型,通常较容易编写,但做得是否足够好且简洁明了,将会极大地影响到你在第二部分中的成功。

译者注:第一点提到的数据模型,即实体类,通常我们放在entity中,或者model,亦或是 domain 包下。

例如,一个用户实体类:

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import lombok.*;

import java.util.List;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class UserData {

    /**
     * 主键
     */
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "c_id")
    private int id;

    /**
     * 登录账号
     */
    @Column(name ="c_username")
    private String username;

    /**
     * 登录密码(加密后)
     */
    @Column(name ="c_password")
    private String password;

    /**
     * 用户昵称
     */
    @Column(name ="c_nickname")
    private String nickname;

    /**
     * 手机号
     */
    @Column(name ="c_telephone")
    private String telephone;

}

大多数人将领域模型实现为我们过去称之为“普通Java对象”这一类型。也就是说,这些是简单的Java类,没有直接依赖于技术基础设施,也没有依赖于处理请求、事务管理、通信或与数据库交互的应用程序逻辑。

在编写这部分代码时,请多花点时间,尽量生成一个与关系数据模型相近的Java模型。在不真正需要的情况下,避免使用奇异或高级的映射特性。如果有任何疑惑不解的地方,请在外键关系的映射上使用@ManyToOne@OneToMany(mappedBy=…​),而不是更复杂的关联映射。

至于上面提到的第二点则要难得多。这部分代码必须:

  • 管理事务和会话,
  • 通过Hibernate会话与数据库交互,
  • 检索并准备UI所需的数据,以及
  • 处理失败(如捕获异常)。

事务和会话管理的责任,以及从某些类型的失败中恢复的责任,最好由某种框架代码来处理。

译者注:第二点提到的代码一般划分为DAO层,亦或是DaoImpl,DAO接口的实现类。

我们很快将回到这个棘手的问题,讨论这种持久性逻辑应该如何组织,以及它应该如何融入系统的其他部分。

1.3.  Hibernate 快速上手

在我们深入了解之前,我们将快速介绍一个基本示例程序,如果你的项目中尚未集成Hibernate,这将帮助你入门。

首先,我们从一个简单的 Gradle 构建文件开始:

译者注:Gradle 和 Maven 都是 Java 项目管理工具,它们用于自动化构建、依赖管理和项目构建的工具。它们的主要目的是简化 Java 项目的构建过程。 Gradle 性能相比 Maven 更加优秀,Spring Boot 也在 2.3.0.M1 版本中对进行了相当重大的更改,开始使用 Gradle 而非 Maven 构建的项目。

build.gradle

plugins {
    id 'java'
}

group = 'org.example'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    // 优秀的ORM
    implementation 'org.hibernate.orm:hibernate-core:6.3.0.Final'

    // Hibernate验证器
    implementation 'org.hibernate.validator:hibernate-validator:8.0.0.Final'
    implementation 'org.glassfish:jakarta.el:4.0.2'

    // Agroal连接池
    implementation 'org.hibernate.orm:hibernate-agroal:6.3.0.Final'
    implementation 'io.agroal:agroal-pool:2.1'

    // 使用Log4j进行日志记录
    implementation 'org.apache.logging.log4j:log4j-core:2.20.0'

    // JPA元模型生成器
    annotationProcessor 'org.hibernate.orm:hibernate-jpamodelgen:6.3.0.Final'

    // HQL的编译时检查
    //implementation 'org.hibernate:query-validator:2.0-SNAPSHOT'
    //annotationProcessor 'org.hibernate:query-validator:2.0-SNAPSHOT'

    // H2数据库
    runtimeOnly 'com.h2database:h2:2.1.214'
}

这些依赖项中,只有第一个是运行 Hibernate 所必需的。

接下来,我们添加一个用于Log4j的日志配置文件:

log4j2.properties

rootLogger.level = info
rootLogger.appenderRefs = console
rootLogger.appenderRef.console.ref = console

logger.hibernate.name = org.hibernate.SQL
logger.hibernate.level = info

appender.console.name = console
appender.console.type = Console
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %highlight{[%p]} %m%n

现在我们需要一些Java代码。我们从实体类开始:

Book.java

package org.hibernate.example;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;

@Entity
class Book {
    @Id
    String isbn;

    @NotNull
    String title;

    Book() {}

    Book(String isbn, String title) {
        this.isbn = isbn;
        this.title = title;
    }
}

最后,让我们看看配置和实例化 Hibernate 的代码,并要求它进行持久化和查询实体。如果现在这些代码看起来一点都不清晰,不要担心。本文旨在让这些内容变得非常清晰明了。

Main.java

package org.hibernate.example;

import org.hibernate.cfg.Configuration;

import static java.lang.Boolean.TRUE;
import static java.lang.System.out;
import static org.hibernate.cfg.AvailableSettings.*;

public class Main {
    public static void main(String[] args) {
        var sessionFactory = new Configuration()
                .addAnnotatedClass(Book.class)
                // 使用H2内存数据库
                .setProperty(URL, "jdbc:h2:mem:db1")
                .setProperty(USER, "sa")
                .setProperty(PASS, "")
                // 使用Agroal连接池
                .setProperty("hibernate.agroal.maxSize", "20")
                // 在控制台显示SQL
                .setProperty(SHOW_SQL, TRUE.toString())
                .setProperty(FORMAT_SQL, TRUE.toString())
                .setProperty(HIGHLIGHT_SQL, TRUE.toString())
                .buildSessionFactory();

        // 导出推断的数据库模式
        sessionFactory.getSchemaManager().exportMappedObjects(true);

        // 持久化一个实体
        sessionFactory.inTransaction(session -> {
            session.persist(new Book("9781932394153", "Hibernate in Action"));
        });

        // 使用HQL查询数据
        sessionFactory.inSession(session -> {
            out.println(session.createSelectionQuery("select isbn||': '||title from Book").getSingleResult());
        });

        // 使用Criteria API查询数据
        sessionFactory.inSession(session -> {
            var builder = sessionFactory.getCriteriaBuilder();
            var query = builder.createQuery(String.class);
            var book = query.from(Book.class);
            query.select(builder.concat(builder.concat(book.get(Book_.isbn), builder.literal(": ")),
                    book.get(Book_.title)));
            out.println(session.createSelectionQuery(query).getSingleResult());
        });
    }
}

在这里,我们使用了 Hibernate 的本地 API 。我们也可以使用 JPA 标准的 API 来达到相同的目的。

1.4. JPA 快速上手

如果我们限制自己只使用 JPA 标准的 API ,我们需要使用 XML 来配置 Hibernate 。

META-INF/persistence.xml

<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">

    <persistence-unit name="example">

        <class>org.hibernate.example.Book</class>

        <properties>

            <!-- H2内存数据库 -->
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:mem:db1"/>

            <!-- 凭据 -->
            <property name="jakarta.persistence.jdbc.user"
                      value="sa"/>
            <property name="jakarta.persistence.jdbc.password"
                      value=""/>

            <!-- Agroal连接池 -->
            <property name="hibernate.agroal.maxSize"
                      value="20"/>

            <!-- 在控制台显示SQL -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.highlight_sql" value="true"/>

        </properties>

    </persistence-unit>
</persistence>

请注意,我们的 build.gradle log4j2.properties 文件保持不变。

我们的实体类与之前的版本也没有改变。

不幸的是,JPA并没有提供 inSession() 方法,所以我们必须自己实现会话和事务管理。我们可以将这些逻辑放入我们自己的 inSession() 函数中,这样我们就不必为每个事务重复这些逻辑。再次强调,你现在不需要理解这些代码的所有内容。

Main.java(JPA版本)

package org.hibernate.example;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;

import java.util.Map;
import java.util.function.Consumer;

import static jakarta.persistence.Persistence.createEntityManagerFactory;
import static java.lang.System.out;
import static org.hibernate.cfg.AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION;
import static org.hibernate.tool.schema.Action.CREATE;

public class Main {
    public static void main(String[] args) {
        var factory = createEntityManagerFactory("example",
                // 导出推断的数据库模式
                Map.of(JAKARTA_HBM2DDL_DATABASE_ACTION, CREATE));

        // 持久化一个实体
        inSession(factory, entityManager -> {
            entityManager.persist(new Book("9781932394153", "Hibernate in Action"));
        });

        // 使用HQL查询数据
        inSession(factory, entityManager -> {
            out.println(entityManager.createQuery("select isbn||': '||title from Book").getSingleResult());
        });

        // 使用Criteria API查询数据
        inSession(factory, entityManager -> {
            var builder = factory.getCriteriaBuilder();
            var query = builder.createQuery(String.class);
            var book = query.from(Book.class);
            query.select(builder.concat(builder.concat(book.get(Book_.isbn), builder.literal(": ")),
                    book.get(Book_.title)));
            out.println(entityManager.createQuery(query).getSingleResult());
        });
    }

    // 在一个会话中执行一些操作,进行正确的事务管理
    static void inSession(EntityManagerFactory factory, Consumer<EntityManager> work) {
        var entityManager = factory.createEntityManager();
        var transaction = entityManager.getTransaction();
        try {
            transaction.begin();
            work.accept(entityManager);
            transaction.commit();
        }
        catch (Exception e) {
            if (transaction.isActive()) transaction.rollback();
            throw e;
        }
        finally {
            entityManager.close();
        }
    }
}

实际上,我们从不会直接在 main() 方法中直接访问数据库。接下来,我们将讨论如何在一个真实系统中组织持久性逻辑。本章的剩余部分不是必须阅读的内容。如果你渴望了解更多关于Hibernate本身的细节,完全可以直接跳到下一章,稍后再回来阅读。

1.5. 编写持久化模块的代码

在一个真实的程序中,像上面展示的代码一样的持久化逻辑通常会与其他类型的代码交织在一起,包括:

  • 实施业务域的规则
  • 与用户交互的逻辑

因此,许多开发人员通常会迅速——甚至过于迅速,在我们看来——寻求将持久化逻辑隔离到某种独立的架构层中。我们现在要求你暂时压制这种冲动。

译者注:太对了....上面就已经开始忍不住划分dao层,entity层了...

使用Hibernate的最简单方法是直接调用 Session 或 EntityManager。如果你是 Hibernate 的新手,使用包装了JPA的框架只会让你的生活变得更加困难。

我们更喜欢一种自底向上的方法来编写我们的代码。我们喜欢思考方法和函数,而不是关于架构层和容器管理的对象。为了说明我们提倡的代码编写方法,让我们考虑一个使用HQL或SQL查询数据库的服务来作为示例。

我们来以下面的代码来举例说明,这是UI和持久化逻辑的混合:

@Path("/") @Produces("application/json")
public class BookResource {
    @GET @Path("book/{isbn}")
    public Book getBook(String isbn) {
        var book = sessionFactory.fromTransaction(session -> session.find(Book.class, isbn));
        return book == null ? Response.status(404).build() : book;
    }
}

事实上,我们可能也会以类似的方式结束——很难具体指出上面的代码有什么问题,对于如此简单的情况,引入额外的对象可能会使这个代码变得更加复杂。

这段代码非常好的一点是,会话和事务管理是由通用的“框架”代码处理的,就像我们上面推荐的那样。在这种情况下,我们使用了内置在 Hibernate 中的 fromTransaction() 方法。但你可能更喜欢使用其他方法,比如:

  • 在像 Jakarta EE 或 Quarkus 这样的容器环境中,使用容器管理的事务和容器管理的持久性上下文
  • 自己编写

重要的是,createEntityManager()getTransaction().begin() 之类的调用不属于常规程序逻辑,因为正确处理错误是棘手而乏味的。

现在让我们考虑一个稍微复杂一点的情况。

@Path("/") @Produces("application/json")
public class BookResource {
    private static final int RESULTS_PER_PAGE = 20;

    @GET @Path("books/{titlePattern}/{page:\\d+}")
    public List<Book> findBooks(String titlePattern, int page) {
        var books = sessionFactory.fromTransaction(session -> {
            return session.createSelectionQuery("from Book where title like ?1 order by title", Book.class)
                    .setParameter(1, titlePattern)
                    .setPage(Page.page(RESULTS_PER_PAGE, page))
                    .getResultList();
        });
        return books.isEmpty() ? Response.status(404).build() : books;
    }
}

这是可以的,如果你愿意,你甚至可以将代码保持原样。但有一件事情我们或许可以改进。我们喜欢非常短的方法,每个方法只做一件事情,并且看起来有一个机会引入一个。让我们使用我们最喜欢的事情——提取方法重构。我们得到了以下的代码:

static List<Book> findBooksByTitleWithPagination(Session session,
                                                 String titlePattern, Page page) {
    return session.createSelectionQuery("from Book where title like ?1 order by title", Book.class)
            .setParameter(1, titlePattern)
            .setPage(page)
            .getResultList();
}

这是一个查询方法的例子,一个接受HQL或SQL查询的参数作为参数的函数,并执行查询,将其结果返回给调用者。这就是它的全部功能;它不协调额外的程序逻辑,也不执行事务或会话管理。

最好的方法是使用 @NamedQuery 注解指定查询字符串,这样 Hibernate 可以在启动时,也就是创建 SessionFactory时,验证查询,而不是在查询第一次执行时。实际上,由于我们在 Gradle 构建(详见1.3. Hibernate快速上手 中 Grable构建)中包含了 Metamodel 生成器(详见 6. Compile-time tooling),查询甚至可以在编译时验证。

我们需要一个地方来放置这个注解,所以让我们将我们的查询方法移动到一个新类中:

@CheckHQL // 在编译时验证命名查询
@NamedQuery(name="findBooksByTitle",
            query="from Book where title like :title order by title")
class Queries {

    static List<Book> findBooksByTitleWithPagination(Session session,
                                                     String titlePattern, Page page) {
        return session.createNamedQuery("findBooksByTitle", Book.class)
                .setParameter("title", titlePattern)
                .setPage(page)
                .getResultList();
    }
}

请注意,我们的查询方法并没有试图将EntityManager隐藏在它的客户端之外。实际上,客户端代码负责向查询方法提供EntityManagerSession。这是我们整个方法的一个非常独特的特性。

客户端代码可能会:

  • 通过调用inTransaction()fromTransaction()获得EntityManagerSession,就像我们上面看到的那样,或
  • 在具有容器管理事务的环境中,可以通过依赖注入获得它。

无论哪种情况,协调一个工作单元的代码通常会直接调用Session或EntityManager,如果需要的话,将它传递给辅助方法,比如我们的查询方法。

@GET
@Path("books/{titlePattern}")
public List<Book> findBooks(String titlePattern) {
    var books = sessionFactory.fromTransaction(session ->
            Queries.findBooksByTitleWithPagination(session, titlePattern,
                    Page.page(RESULTS_PER_PAGE, page)));
    return books.isEmpty() ? Response.status(404).build

你可能会觉得我们的查询方法看起来有点样板化。这是正确的,但我们更关心的是它不够类型安全。实际上,多年来,HQL查询和将参数绑定到查询参数的代码缺乏编译时检查一直是我们对 Hibernate 不满意的主要原因。

幸运的是,现在有了解决这两个问题的方法:在Hibernate 6.3的试验性功能中,我们现在提供了使用元模型生成器为你填充这种查询方法实现的可能性。这个功能是本介绍的一个完整章节的主题,所以现在我们只给你留下一个简单的例子。

假设我们将Queries简化为如下所示:

interface Queries {
    @HQL("where title like :title order by title")
    List<Book> findBooksByTitleWithPagination(String title, Page page);
}

然后,元模型生成器会自动生成一个带有@HQL注解方法的实现,它位于一个名为 Queries_ 的类中。我们可以像调用我们手写的版本一样调用它:

@GET
@Path("books/{titlePattern}")
public List<Book> findBooks(String titlePattern) {
    var books = sessionFactory.fromTransaction(session ->
            Queries_.findBooksByTitleWithPagination(session, titlePattern,
                    Page.page(RESULTS_PER_PAGE, page));
    return books.isEmpty() ? Response.status(404).build() : books;
}

在这种情况下,消除的代码量相当小。真正的价值在于提高了类型安全性。现在,在将参数分配给查询参数时,我们会在编译时发现错误。

此时,我们相信你对这个想法充满了疑虑。这是完全合理的。我们很愿意在这里回答你的异议,但那会让我们离题太远。所以我们请你暂时搁置这些想法。我们承诺在适当的时候会让它变得合理。并且,在那之后,如果你仍然不喜欢这种方法,请理解它是完全可选的。没有人会来你家强制你接受它。

现在我们大致了解了我们的持久化逻辑可能是什么样子,自然而然地,我们会问如何测试我们的代码。

1.6. 测试持久化逻辑

当我们为持久化逻辑编写测试时,我们需要:

  • 一个数据库
  • 由我们持久化实体映射的模式的实例
  • 一组测试数据,在每个测试开始时处于良好定义的状态。

或许很明显我们应该使用与我们在生产中将要使用的相同的数据库系统进行测试,而且我们肯定应该为这种配置编写一些测试。但另一方面,执行I/O操作的测试比那些不执行I/O操作的测试要慢得多,而且大多数数据库无法配置为在进程内运行。

因此,由于使用Hibernate 6编写的大多数持久化逻辑在不同数据库之间具有极高的可移植性,通常情况下,对于内存中的Java数据库进行测试是有意义的(我们推荐使用H2数据库)。

但是,如果我们的持久化代码使用原生SQL,或者使用悲观锁之类的并发管理特性,我们在这里需要小心。

无论我们是针对真实数据库进行测试还是针对内存中的Java数据库进行测试,我们都需要在测试套件开始时导出模式。我们通常在创建Hibernate SessionFactory或JPA EntityManager时执行此操作,因此传统上我们使用了一个配置属性。

JPA标准的属性是 jakarta.persistence.schema-generation.database.action 。例如,如果我们使用 Configuration 配置 Hibernate,我们可以这样写:

configuration.setProperty(AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION,
 Action.SPEC_ACTION_DROP_AND_CREATE);

或者,在Hibernate 6中,我们可以使用新的SchemaManager API来导出模式,就像我们之前所做的那样。

sessionFactory.getSchemaManager().exportMappedObjects(true);

由于在许多数据库上执行DDL语句非常慢,我们不希望在每个测试之前都执行这个操作。相反,为了确保每个测试始于具有良好定义状态的测试数据,我们需要在每个测试之前执行两件事:

  1. 清理前一个测试留下的任何混乱,然后
  2. 重新初始化测试数据。

我们可以使用SchemaManager截断所有表,使数据库模式为空。

sessionFactory.getSchemaManager().truncateMappedObjects();

在截断表之后,我们可能需要初始化我们的测试数据。我们可以在一个SQL脚本中指定测试数据,例如:

import.sql

insert into Books (isbn, title) values ('9781932394153', 'Hibernate in Action');
insert into Books (isbn, title) values ('9781932394887', 'Java Persistence with Hibernate');
insert into Books (isbn, title) values ('9781617290459', 'Java Persistence with Hibernate, Second Edition');

如果我们将此文件命名为 import.sql ,并将其放置在根类路径中,那就是我们需要做的一切。

否则,我们需要在配置属性 jakarta.persistence.sql-load-script-source 中指定该文件。如果我们使用 Configuration 配置 Hibernate ,我们可以这样写:

configuration.setProperty(AvailableSettings.JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE, "/org/example/test-data.sql");

每次调用 exportMappedObjects() truncateMappedObjects() 时,都将执行SQL脚本。

还有另一种混乱可能由测试留下:二级缓存中的缓存数据。我们建议在大多数类型的测试中禁用Hibernate的二级缓存。或者,如果没有禁用二级缓存,那么在每个测试之前我们应该调用:

sessionFactory.getCache().evictAllRegions();

现在,假设你遵循了我们的建议,将你的实体和查询方法编写得尽量减少对“基础设施”(即除了JPA和Hibernate之外的库、框架、容器管理的对象甚至是你自己系统的难以从头创建的部分)的依赖。那么,测试持久化逻辑就会变得非常简单!

你需要做的是:

  1. 在测试套件的开始时引导 Hibernate 并创建一个 SessionFactory 或 EntityManagerFactory(我们已经知道如何做了)
  2. 在每个 @Test 方法中创建一个新的 Session 或 EntityManager ,例如使用 inTransaction() 

实际上,有些测试可能需要多个会话。但要小心不要在不同的测试之间泄漏会话。

我们还需要进行的另一个重要测试是验证我们的O/R映射注解与实际数据库模式的匹配情况。这再次是模式管理工具的职责,要么是:

configuration.setProperty(AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, Action.ACTION_VALIDATE);

或是

sessionFactory.getSchemaManager().validateMappedObjects();

这个“测试”在很多情况下人们喜欢在生产中启动系统时运行。

1.7. 架构与持久化层

现在让我们考虑一种不同的代码组织方式,这是我们对其持怀疑态度的方式。

在本节中,我们将提供我们的观点。如果你只对事实感兴趣,或者如果你宁愿不阅读可能动摇你目前观点的东西,请随时跳到下一章。

Hibernate是一个与架构无关的库,而不是一个框架,因此与各种Java框架和容器很好地集成。与我们在生态系统中的位置一致,我们历来避免在架构方面提供太多建议。这是一种我们现在可能倾向于后悔的做法,因为由于缺乏建议,结果产生了来自那些提倡架构、设计模式和额外框架的人的建议,我们怀疑这些建议使得使用Hibernate变得不那么愉快。

特别是那些包装JPA的框架似乎增加了冗余代码,同时减少了Hibernate所提供的对数据访问的精细控制。这些框架没有暴露Hibernate的全部功能集,因此程序被迫使用一个功能较弱的抽象。

我们有点怀疑的是,那种古板、教条的传统智慧是:

与数据库交互的代码应该位于单独的持久化层。

我们缺乏勇气——甚至可能是信念——坚定地告诉你不要遵循这个建议。但我们请你考虑一下任何架构层的样板代码成本,以及这种成本是否在你的系统环境中真的值得。

为了给这个讨论增加一些背景,尽管有可能我们的介绍在一个很早的阶段就沦为了对这种观点的咆哮,我们要求你在听我们多说一点古老历史的同时,请容忍我们。

一个史诗般的DAOs和Repositories故事

在Java EE 4的黑暗时代,在Hibernate标准化之前以及JPA在Java企业开发中的日益普及之前,手工编写Hibernate现在负责的那些凌乱的JDBC交互是很常见的。在那个可怕的时期,出现了一个我们过去称之为数据访问对象(DAOs)的模式。DAO给了你一个放置所有那些令人讨厌的JDBC代码的地方,留下了重要的程序逻辑更清晰。

当Hibernate突然在2001年出现时,开发人员非常喜欢它。但Hibernate没有实现任何规范,许多人希望减少或至少将项目逻辑对Hibernate的依赖局限在某个范围内。一个明显的解决方案是保留DAOs,但是将它们内部的JDBC代码替换为对Hibernate Session的调用。

我们在这件事情上的角色也是有些责任的。

在2002年和2003年,这似乎真的是一种相当合理的做法。实际上,我们通过推荐(或者至少不阻止)在《Hibernate in Action》中使用DAOs的方式,为这种方法的流行做出了贡献。我们在这里对这个错误表示歉意,以及对我们花了太长时间才意识到这个错误表示歉意。

最终,一些人开始相信他们的DAOs使他们的程序免于依赖ORM,使他们能够在需要时用JDBC或其他东西替换掉Hibernate。事实上,这并不是真的——在每次与数据库的交互都是显式和同步的JDBC编程模型与Hibernate中的有状态会话的编程模型之间存在相当深的差异,其中更新是隐式的,SQL语句是异步执行的。

但是,整个Java中的持久性的风景在2006年4月发生了变化,当时JPA 1.0的最终草案得到了批准。Java现在有了一种标准的ORM方式,具有多个高质量的标准API的实现。DAOs的时代终结了,对吗?

嗯,不是的。不是的。DAOs被重新命名为“repositories”,并且继续作为连接到JPA的前端而活在世上。但是它们真的发挥了作用吗,还是它们只是多余的额外复杂性和膨胀?它们是一种使堆栈跟踪更难以阅读、代码更难以调试的额外间接层吗?

我们的观点是,它们主要是多余的。JPA EntityManager就是一个“repository”,它是一个标准的、由整天思考持久性问题的人编写的有明确定义规范的存储库。如果这些存储库框架提供了实际有用的东西——并且不是显然会导致问题的东西——超出了EntityManager提供的功能,我们几十年前就已经将它添加到EntityManager中了。

最终,我们不确定你是否真的需要一个单独的持久化层。至少考虑一下可能直接从你的业务逻辑中调用EntityManager可能是可以接受的。

译者注:很新奇的观点,我已经习惯有DAO层了...

API概览

我们已经听到你在对我们的异端邪说嘶嘶声了。但在你砰然关闭笔记本电脑的盖子,去找蒜和草叉之前,花点时间来权衡一下我们提出的观点。

好吧,如果这让你感觉好一些,将EntityManager视为一个适用于系统中每个实体的单一通用“repository”是一种方式。从这个角度看,JPA就是你的持久化层。将这个抽象包装在一个较不通用的第二个抽象中是否真的有必要呢?

即使一个独立的持久化层是合适的,DAO风格的存储库也不是唯一明确正确的分解方程的方式:

  1. 大多数复杂一点的查询涉及多个实体,因此很难确定这样一个查询属于哪个存储库
  2. 大多数查询对特定的程序逻辑片段非常具体,并且在系统中的不同位置之间不会被重用。
  3. 一个存储库的各种操作很少相互作用或共享共同的内部实现细节。

事实上,存储库本质上具有非常低的内聚性。如果每个存储库都有多个实现,那么存储库对象的层次可能是有意义的,但实际上几乎没有人这样做。因为它们也与客户端高度耦合,具有非常大的API表面。相反,只有当一个层的API非常窄时,它才容易替换。

有些人确实使用模拟存储库进行测试,但我们确实很难看到这种做法有任何价值。如果我们不想对我们的真实数据库运行测试,通常可以通过在内存中的Java数据库(如H2)上运行测试来“模拟”数据库。在Hibernate 6中,这比在旧版本的Hibernate中更好,因为HQL现在在平台之间更具可移植性。

哎呀,让我们继续吧。

1.8. 概览

现在是时候开始我们的旅程,真正理解我们之前看到的代码了。

这个介绍将引导你完成使用Hibernate进行持久化的程序开发中涉及的基本任务:

  1. 配置和引导Hibernate,并获取SessionFactory或EntityManagerFactory的实例,
  2. 编写领域模型,即一组表示程序中持久类型的实体类,并将其映射到数据库的表,
  3. 当模型映射到现有关系模式时,自定义这些映射,
  4. 使用Session或EntityManager执行查询数据库并返回实体实例的操作,或者更新数据库中的数据,
  5. 使用Hibernate元模型生成器提高编译时类型安全性,
  6. 使用Hibernate查询语言(HQL)或本地SQL编写复杂查询,最后
  7. 调优数据访问逻辑的性能。

当然,我们将从这个列表的顶部开始,即最不有趣的主题:配置。

FusionCompute 6.3.1 cna.iso 是华为云推出的一种软件镜像文件。cna.iso 是 Cloud Native Architecture(云原生架构)的缩写,是华为云云原生计算产品 FusionCompute 的安装镜像文件。FusionCompute 是华为云的一种云计算虚拟化平台,基于云原生架构构建,提供企业级的云计算解决方案。 cna.iso 是 FusionCompute 6.3.1 版本的安装镜像文件,它包含了安装 FusionCompute 所需的软件和组件。通过 cna.iso 文件,用户可以在物理服务器上进行安装,并将其转化为虚拟化主机,以便在上面部署和管理虚拟机和容器。 FusionCompute 6.3.1 版本是华为云云原生计算产品的一个重要更新版本,带来了许多新的功能和改进。它包括了更强大的虚拟化管理能力、高可用性和可靠性、性能优化以及安全增强等方面的改进。此外,FusionCompute 还支持基于 Kubernetes 的容器管理平台,提供了更便捷的容器化部署和管理方式。 使用 FusionCompute 6.3.1 cna.iso,用户可以快速搭建起高性能、可靠的云计算基础设施,提供灵活的资源分配和管理。它可以广泛应用于各种企业和组织,满足不同规模和需求的云计算场景。通过 FusionCompute,用户可以更高效地利用硬件资源,降低 IT 成本,提升业务的灵活性和可扩展性。 总之,FusionCompute 6.3.1 cna.iso 是华为云云原生计算产品 FusionCompute 的安装镜像文件,它为用户提供了搭建高性能、可靠的云计算基础设施的能力,以满足不同企业和组织的云计算需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风间琉璃c

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值