Hibernate ORM 中文操作指南

1. Introduction

Hibernate 通常被描述为一个库,它可以轻松地将 Java 类映射到关系数据库表。但这并不能说明关系数据本身所扮演的核心角色。因此,更准确的描述可能是:

这里,重点是关系数据以及类型安全的重要性。object/relational mapping(ORM)的目标是消除脆弱且不类型安全的代码,并从长远来看,使大型程序更容易维护。

ORM 通过解除开发人员编写单调、重复且脆弱的代码以将对象图扁平化至数据库表并将对象图从平面 SQL 查询结果集重建出来的需要,从而消除了持久化的痛苦。更好的是,ORM 使得在已编写基本持久化逻辑后,在今后调整性能更加容易。

一个常年的问题是:我应该使用 ORM 还是纯 SQL?答案通常是:use both。JPA 和 Hibernate 被设计为可以处理 in conjunction with 手写 SQL。您会看到,大多数具有复杂数据访问逻辑的程序都会受益于使用 ORM,至少 @{10}。但是,如果 Hibernate 使事情变得更加困难,对于某些特别棘手的数据访问逻辑,唯一明智的做法就是使用更适合该问题的解决方案!仅仅因为您正在使用 Hibernate 进行持久化并不意味着您必须将其用于 @{11}。

开发人员经常询问 Hibernate 与 JPA 之间的关系,因此让我们简要回顾一下历史。

1.1. Hibernate and JPA

Hibernate 是 Java 持久性 API(现在是 JakartaPersistence API(或 JPA)背后的灵感,并且包含了该规范的最新版本的完整实现。

我们可以从三个基本要素的角度来考虑 Hibernate 的 API:

  1. JPA 定义的 API 的实现,最重要的是接口 _EntityManagerFactory_和 _EntityManager_以及 JPA 定义的 O/R 映射注释,

  2. native API 公开了所有可用的功能集,其核心接口为 SessionFactory ,继承自 EntityManagerFactory ,以及 Session ,继承自 EntityManager ,以及

  3. 一组 mapping annotations ,它扩充了 JPA 定义的 O/R 映射注解,可以与 JPA 定义的接口或本机 API 一起使用。

Hibernate 还为扩展或集成 Hibernate 的框架和库提供了一系列 SPI,但我们这里对这些东西都不感兴趣。

api overview

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

  1. 编写你的程序要使用 Session_和 _SessionFactory,或

  2. 最大限度地提高与 JPA 其他实现的移植性,通过在合理的情况下编写 _EntityManager_和 _EntityManagerFactory_的代码,仅在必要时回退到原生 API。

无论您采用哪种路径,大多数情况下都将使用 JPA 定义的映射注解,而对于更高级的映射问题则使用 Hibernate 定义的注解。

您可能会想,使用 only JPA 定义的 API 来开发应用程序是否可行,事实上从理论上是可行的。JPA 非常出色,它确实抓住了对象/关系映射问题的基本原理。但是,如果没有本机 API 和扩展映射注释,您将错过 Hibernate 的很多强大功能。

由于 Hibernate 早于 JPA 存在,并且由于 JPA 是基于 Hibernate 建模的,因此我们在标准 API 和原生 API 之间的命名中不幸遇到了一些竞争和重复。例如:

表 1. 具有相似命名方式的竞争 API 示例

Hibernate

JPA

org.hibernate.annotations.CascadeType

javax.persistence.CascadeType

org.hibernate.FlushMode

javax.persistence.FlushModeType

org.hibernate.annotations.FetchMode

javax.persistence.FetchType

org.hibernate.query.Query

javax.persistence.Query

org.hibernate.Cache

javax.persistence.Cache

@org.hibernate.annotations.NamedQuery

@javax.persistence.NamedQuery

@org.hibernate.annotations.Cache

@javax.persistence.Cacheable

通常,与 JPA 相比,原生的 Hibernate API 提供了一点额外的功能,因此这并不完全是 flaw。但这需要注意。

1.2. Writing Java code with Hibernate

如果您完全不了解 Hibernate 和 JPA,您可能已经在思考与持久性相关的代码如何构建。

嗯,通常情况下,我们的与持久性相关的代码有以下两个层:

  • 对 Java 中数据模型的表示,它采取一组带注释的实体类形式,以及

  • 大量与 Hibernate 的 API 交互的函数,以执行与各种事务关联的持久性操作。

第一部分,即数据或“域”模型,通常更容易编写,但是出色地、干净利落地完成这项工作会极大地影响您在第二部分的成功。

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

花时间研究此代码,并尝试生成一个与关系数据模型尽可能接近、合理化的 Java 模型。避免使用稀奇或高级的映射功能(如果真的不需要的话)。当有丝毫疑问时,使用 @ManyToOne 优先于 @OneToMany(mappedBy=…​) 映射外键关系,以获得更复杂的关联映射。

代码的第二部分更难正确编写。此代码必须:

  1. manage transactions and sessions,

  2. 通过 Hibernate 会话与数据库交互,

  3. 获取并准备用户界面需要的数据,以及

  4. handle failures.

最好在某种框架代码中处理事务和会话管理,以及某些故障类型的恢复。

我们正在 come back soon 一个关于此持久性逻辑应该如何组织、以及如何适应系统其余部分的棘手问题。

1.3. Hello, Hibernate

在我们深入探讨这些问题之前,我们将快速提供一个基本的示例程序,如果您尚未将 Hibernate 集成到您的项目中,这将帮助您快速入门。

我们从一个简单的 gradle 构建文件开始:

build.gradle
plugins {
    id 'java'
}

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

repositories {
    mavenCentral()
}

dependencies {
    // the GOAT ORM
    implementation 'org.hibernate.orm:hibernate-core:7.0.0.Beta1'

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

    // Agroal connection pool
    implementation 'org.hibernate.orm:hibernate-agroal:7.0.0.Beta1'
    implementation 'io.agroal:agroal-pool:2.1'

    // logging via Log4j
    implementation 'org.apache.logging.log4j:log4j-core:2.20.0'

    // JPA Metamodel Generator
    annotationProcessor 'org.hibernate.orm:hibernate-processor:7.0.0.Beta1'

    // Compile-time checking for HQL
    //implementation 'org.hibernate:query-validator:2.0-SNAPSHOT'
    //annotationProcessor 'org.hibernate:query-validator:2.0-SNAPSHOT'

    // H2 database
    runtimeOnly 'com.h2database:h2:2.1.214'
}

其中,只有第一个依赖项对运行 Hibernate 是绝对 required

接下来,我们将为 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 代码。我们从 entity class 开始:

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)
                // use H2 in-memory database
                .setProperty(URL, "jdbc:h2:mem:db1")
                .setProperty(USER, "sa")
                .setProperty(PASS, "")
                // use Agroal connection pool
                .setProperty("hibernate.agroal.maxSize", 20)
                // display SQL in console
                .setProperty(SHOW_SQL, true)
                .setProperty(FORMAT_SQL, true)
                .setProperty(HIGHLIGHT_SQL, true)
                .buildSessionFactory();

        // export the inferred database schema
        sessionFactory.getSchemaManager().exportMappedObjects(true);

        // persist an entity
        sessionFactory.inTransaction(session -> {
            session.persist(new Book("9781932394153", "Hibernate in Action"));
        });

        // query data using HQL
        sessionFactory.inSession(session -> {
            out.println(session.createSelectionQuery("select isbn||': '||title from Book").getSingleResult());
        });

        // query data using 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. Hello, 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 in-memory database -->
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:mem:db1"/>

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

            <!-- Agroal connection pool -->
            <property name="hibernate.agroal.maxSize"
                      value="20"/>

            <!-- display SQL in console -->
            <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.gradlelog4j2.properties 文件没有更改。

我们的实体类也没有任何改动。

遗憾的是,JPA 并未提供 inSession() 方法,所以我们必须自己实现会话和事务管理。我们可以将该逻辑放在我们自己的 inSession() 函数中,以避免为每个事务重复该逻辑。同理,您也不需要立刻理解这段代码。

Main.java (JPA version)
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",
                // export the inferred database schema
                Map.of(JAKARTA_HBM2DDL_DATABASE_ACTION, CREATE));

        // persist an entity
        inSession(factory, entityManager -> {
            entityManager.persist(new Book("9781932394153", "Hibernate in Action"));
        });

        // query data using HQL
        inSession(factory, entityManager -> {
            out.println(entityManager.createQuery("select isbn||': '||title from Book").getSingleResult());
        });

        // query data using 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());
        });
    }

    // do some work in a session, performing correct transaction management
    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 本身的更多详细信息,非常欢迎您直接跳到 next chapter,稍后再回来。

1.5. Organizing persistence logic

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

  1. 实现业务领域的规则或

  2. 用于与用户交互。

因此,许多开发人员很快就会——甚至 too quickly,我们认为——找到方法将持久化逻辑隔离到某种单独的架构层中。我们建议您暂时抑制这种冲动。

使用 Hibernate 的 easiest 方式是直接调用 SessionEntityManager。如果您不熟悉 Hibernate,那么包含 JPA 的框架只会让您的生活变得更困难。

我们更喜欢 bottom-up 方法来组织我们的代码。我们喜欢先考虑方法和函数,而不是考虑架构层和容器管理的对象。为了说明我们提倡的代码组织方法,让我们考虑一个使用 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;
    }
}

事实上,我们也可能会 finish 类似这样的东西——很难具体找出上面代码的任何问题,并且对于这样简单的案例,通过引入其他对象来让这段代码更复杂似乎真的很困难。

我们希望引起你注意的是,此代码的一个非常好的方面是,会话和事务管理是由通用的“框架”代码处理的,正如我们上面已经建议的那样。在本例中,我们使用 fromTransaction() 方法,它恰好是 Hibernate 中内置的。但是,你可能更喜欢使用其他内容,例如:

  1. 在 Jakarta EE 或 Quarkus 等容器环境中,container-managed transactions_和 _container-managed persistence contexts,或

  2. something you write yourself.

重要的是,像 createEntityManager()getTransaction().begin() 这样的调用不属于常规程序逻辑,因为正确执行错误处理很棘手且很乏味。

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

@Path("/") @Produces("application/json")
public class BookResource {
    private static final 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;
    }

}

这很好,如果您希望将代码保持原样,我们也不会抱怨。但有一件事我们也许可以改进。我们喜欢具有单一职责的超短方法,并且这里似乎有机会介绍一个这样的方法。让我们使用我们最喜欢的方法,Extract Method 重构,来处理代码。我们得到:

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();
}

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

使用 @NamedQuery 注解指定查询字符串会更好,这样 Hibernate 可以在启动时验证此查询,也就是在创建 SessionFactory 时,而不是在初次执行查询时进行验证。事实上,由于我们在 Gradle build 中包括了 Metamodel Generator,因此甚至可以在 compile time 验证查询。

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

@CheckHQL // validate named queries at compile time
@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。这是我们整个方法的一个很特别的特征。

客户端代码可能:

  1. 通过调用 inTransaction()_或 _fromTransaction(),获取一个 EntityManager_或 _Session,如上所述,或

  2. 在具有容器管理事务的环境中,它可以通过依赖注入来获取它。

无论哪种情况,协调工作单元的代码通常只直接调用 SessionEntityManager,必要时将其传递给诸如我们的查询方法之类的帮助程序方法。

@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;
}

您可能认为我们的查询方法看起来有点像样板。也许是这样,但我们更担心它不是类型安全的。事实上,多年来,缺少对 HQL 查询和将参数绑定到查询参数的代码的编译时检查一直是我们对 Hibernate 不满的主要来源。

幸运的是,现在有两个问题的解决方案:作为 Hibernate 6.3 的一项孵化功能,我们现在提供让元模型生成器为您填充此类查询方法的实现的可能性。此功能是 a whole chapter of this introduction 的主题,所以现在我们只给您一个简单的例子。

假设我们将 Queries 简化为以下内容:

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

那么 Metamodel Generator 会自动生成一个名为 Queries_ 的类中 @HQL 注释的方法的实现。我们可以像调用我们手写的版本一样调用它:

@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;
}

在这种情况下,消除的代码数量十分微不足道。真正有价值的是更高的类型安全性。我们现在在编译时便可找出查询参数赋值中的错误。

在这一点上,我们确信你对此想法有很多疑问。这是理所当然的。我们很想在此处解答你的疑虑,但这会让我们偏离正轨。因此,我们要求你暂时将这些想法归档。我们保证在我们 properly address this topic later 时会让它讲得通。在那之后,如果你仍然不喜欢这种方法,请理解这是一项完全可选的操作。不会有人来你家强迫你接受。

既然我们对持久性逻辑的样子有一个大概的了解,那么自然就会问我们应该如何测试代码。

1.6. Testing persistence logic

在为持久性逻辑编写测试时,我们将需要:

  • a database, with

  • 一个由我们的持久性实体映射的模式实例,以及

  • 一组测试数据,在每次测试开始时都处于明确定义的状态。

显而易见,我们应该针对将在生产中使用的同一数据库系统进行测试,并且实际上我们肯定应该至少对此配置进行 some 测试。但是另一方面,执行 I/O 的测试比不执行 I/O 的测试要慢得多,而且大多数数据库都无法设置成在进程内运行。

因此,由于使用 Hibernate 6 编写的持久化逻辑在数据库之间具有 extremely 可移植性,因此在内存 Java 数据库中进行测试通常是一个好主意。( H2 是我们推荐的。)

如果我们的持久性代码使用本机 SQL,或者如果它使用悲观锁之类的并发管理功能,则我们确实需要小心。

无论我们是针对我们的真实数据库进行测试,还是针对内存中 Java 数据库进行测试,我们都需要在测试套件开始时导出模式。当我们创建 Hibernate SessionFactory 或 JPA EntityManager 时,我们 usually 这样做,所以传统上我们为此使用了 configuration property

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 来导出模式,就像我们执行 above 所做的那样。

sessionFactory.getSchemaManager().exportMappedObjects(true);

由于在许多数据库上执行 DDL 语句非常慢,因此我们不会在每次测试前进行此操作。相反,为了确保每个测试都以明确定义的状态开始测试数据,我们需要在每次测试前做两件事:

  • 清理前一个测试留下的任何混乱,然后

  • reinitialize the test data.

我们可能会清空所有表,留下一个空的数据库模式,方法采用 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,并把它放在根类路径中,这就完成了我们要做的。

否则,我们需要在 configuration property jakarta.persistence.sql-load-script-source 中指定文件。如果我们使用 Configuration 来配置 Hibernate,我们可以编写:

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

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

测试可能会留下另一种混乱: second-level cache 中的缓存数据。我们建议对大多数类型的测试 disabling Hibernate 的二级缓存。或者,如果二级缓存未禁用,则在每个测试之前我们应该调用:

sessionFactory.getCache().evictAllRegions(); sessionFactory.getCache().evictAllRegions();

现在,假设你遵循了我们的建议,编写了实体和查询方法以最大程度地减少对“基础设施”的依赖,即依赖于 JPA 和 Hibernate 之外的库、框架、容器管理对象,甚至依赖于难以从头实例化的你自己的系统部分。这样,测试持久性逻辑就是直接的了!

你需要:

  1. 引导 Hibernate 并创建一个 _SessionFactory_或 _EntityManagerFactory_以及测试套件的开头(我们已经了解了如何执行此操作),以及

  2. 使用 inTransaction()_在每个 _@Test_方法中创建一个新的 _Session_或 _EntityManager

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

另外一个我们需要的重要的测试是针对实际数据库模式验证我们的 O/R mapping annotations 。这再次是模式管理工具的任务,要么是:

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

sessionFactory.getSchemaManager().validateMappedObjects(); sessionFactory.getSchemaManager().validateMappedObjects(); 许多人甚至在生产环境中也喜欢在系统启动时运行此“测试”。

1.7. Architecture and the persistence layer

现在让我们考虑一种不同的代码组织方法,一种让我们心生怀疑的方法。

在本节中,我们将向你提供我们的 opinion。如果你只对事实感兴趣,或者如果你不想阅读可能破坏你当前观点的信息,请跳至 next chapter

Hibernate 是一个与架构无关的库,而不是一个框架,因此可以与广泛的 Java 框架和容器集成。与我们在生态系统中的位置一致的是,我们历来避免就架构提出太多建议。这是一个我们现在可能倾向于后悔的做法,因为由此产生的真空已经被提倡架构、设计模式和我们认为会使 Hibernate 的使用变得不太愉快且不必要的额外框架的人们的建议所填补。

尤其是,包装 JPA 的框架似乎增加了臃肿,而减弱了 Hibernate 努力提供的对数据访问的细化控制。这些框架不会公开 Hibernate 的完整特性集,因此程序被迫使用不那么强大的抽象。

我们犹豫着质疑这种呆板、教条的 conventional 智慧,只是因为害怕在招惹这种教条的时候刺伤自己不可避免地竖起的逆毛:

我们缺乏勇气,甚至信念,来明确告诉你 not 遵循此建议。但是,我们确实要求你考虑在任何体系架构层中样板的成本,以及这种成本所带来的收益是否真的值得你的系统。

为了给这次讨论添加一些背景信息,并且冒着我们的简介在如此早期的阶段变成咆哮的风险,我们要请你容忍一下,同时我们更多地谈论一些往事。

最终,我们不确定你是否还需要一个单独的持久性层。至少 consider,从业务逻辑中直接调用 EntityManager 可能是没有问题的。

architecture

我们已经能听到你对我们的异端发出嘘声。但在摔上你笔记本的盖子并冲出去取大蒜和干草叉之前,花几个小时权衡一下我们的提议。

好吧,所以,看看吧,如果这让你感觉更好,查看 EntityManager 的一种方法是将其视为一个单独的 generic “仓库”,它适用于系统中的每个实体。从这个角度来看,JPA is 你的持久性层。而且几乎没有理由用一个 less 通用、抽象的第二个抽象层将这个抽象包裹起来。

即使在明显适当的持久性层中,DAO 样式的仓库也不是对等式分解最明确最正确的方法:

  1. 大多数非平凡查询会触及多个实体,因此通常会含糊不清此类查询属于哪个存储库,

  2. 大多数查询对于程序逻辑的特定片段极其具体,并且不会在系统中的不同位置重复使用,以及

  3. 存储库的各种操作很少交互或共享常见的内部实现详细信息。

事实上,仓库本质上表现出非常低的 cohesion。如果你有每个仓库的多个实现,一个仓库对象层可能有意义,但在实践中几乎没有人这样做。这是因为它们对它们的客户端也是极其 coupled 的,具有非常大的 API 表面。相反,如果它具有非常 narrow 的 API,一个层才容易替换。

有些人确实使用模拟存储库进行测试,但我们确实很难从中看出任何价值。如果我们不想针对我们的真实数据库运行测试,通常可以通过针对 H2 之类的内存 Java 数据库运行测试来“模拟”数据库本身。在 Hibernate 6 中,这种做法比在 Hibernate 的较早版本中效果更好,因为 HQL 现在 much 在不同平台间更易于移植。

Phew,让我们继续。

1.8. Overview

现在是开始 understanding 我们之前看到的代码的旅程的时候了。

本引言将指导你完成开发使用 Hibernate 进行持久性的程序所涉及的基本任务:

  • 配置和引导 Hibernate,并获取 _SessionFactory_或 _EntityManagerFactory_的实例,

  • 编写 domain model,即一组 entity classes,它表示程序中的持久性类型,并且映射到你的数据库的表,

  • Customize 这些映射时模型映射到预先存在的关联模式,

  • 使用 SessionEntityManager 执行查询数据库并返回实体实例或更新存储在数据库中的数据的操作,

  • 使用 Hibernate 元模型生成器来改善编译时的类型安全性,

  • 使用 Hibernate 查询语言 (HQL) 或本机 SQL 编写复杂查询,最后

  • 调整数据访问逻辑的性能。

当然,我们要从这个列表的开头、最无趣的主题开始:configuration