序言

Jakarta Data 是为存储库制定的新规范。此处的存储库指的是公开一个类型安全的 API 以与数据存储进行交互的接口。Jakarta Data 被设计为可容纳各种各样的数据库技术,从关系型数据库到文档数据库,到键值存储乃至更多。

Hibernate Data Repositories 是 Jakarta Data 的一种面向关系型数据库的实现,其后端由 Hibernate ORM 提供支持。实体类别使用 Jakarta Persistence 定义的熟悉的注释进行映射,并且查询可以使用 Hibernate 查询语言(Jakarta Persistence 查询语言 (JPQL) 的超集)来编写。但另一方面,在 Jakarta Data 中与数据库交互的编程模型与你可能习惯从 Jakarta Persistence 中使用的模型有着很大的不同。

因此,本指南将向你展示使用 Hibernate 的一种新方式。

对 Jakarta Data 的介绍并没有包罗所有信息。如果你想了解全部信息,则应在阅读本指南的同时参阅该规范,我们努力让它易于理解。

1. 编程模型

Jakarta Data 和 Jakarta Persistence 都使用实体类别以类型安全的方式来表示数据。由于 Hibernate 对 Jakarta Data 的实现以访问关系型数据库为后盾,因此这些实体类别是使用 Jakarta Persistence 定义的注释进行映射的。

例如

@Entity
public class Book {
    @Id
    String isbn;

    @Basic(optional = false)
    String title;

    LocalDate publicationDate;

    @Basic(optional = false)
    String text;

    @Enumerated(STRING)
    @Basic(optional = false)
    Type type = Type.Book;

    @ManyToOne(optional = false, fetch = LAZY)
    Publisher publisher;

    @ManyToMany(mappedBy = Author_.BOOKS)
    Set<Author> authors;

    ...
}

@Entity
public class Author {
    @Id
    String ssn;

    @Basic(optional = false)
    String name;

    Address address;

    @ManyToMany
    Set<Book> books;
}

有关实体映射的更多信息,请参阅Hibernate 6 简介

Jakarta Data 还可以使用 Jakarta NoSQL 定义的同名注释定义的实体。但在本指南中,我们使用的是 Hibernate Data Repositories,因此应将所有映射注释理解为在 jakarta.persistenceorg.hibernate.annotations 中定义的注释。有关 Jakarta Data 中的实体的更多信息,请查阅该规范的第 3 章。

此外,查询可以用 HQL(即 Hibernate 的 Jakarta Persistence 查询语言 (JPQL) 超集)来表达。

Jakarta Data 规范定义了一个简单的 JPQL 子集,恰如其分地称为 JDQL。JDQL 主要与非关系型数据存储相关;预期由访问关系型数据的 Jakarta Data 实现支持 JPQL 的一个大得多子集。事实上,Hibernate Data Repositories 支持 JPQL 的超集。因此,即使我们在倡导、设计和指定 JDQL 方面付出了相当大的努力,但我们在这里不会详细介绍。有关 JDQL 的信息,请参阅 Jakarta Data 规范的第 5 章。

要了解有关 HQL 和 JPQL 的更多信息,请参阅 Hibernate 查询语言指南

Jakarta Persistence 和 Jakarta Data 的相似之处到此结束。下表对比了这两种编程模型。

持久性 数据

持久性上下文

有状态

无状态

网关

EntityManager 接口

用户编写的 @Repository 接口

底层实现

会话

无状态会话

持久性操作

诸如 find()persist()merge()remove() 等通用方法

带有 @Find@Insert@Update@Save@Delete 注解的类型安全用户编写方法

SQL 执行

在刷新期间

立即

更新

通常为隐式(在刷新期间进行脏检查)

始终显式(通过调用 @Update 方法)

操作级联

取决于 CascadeType

从不

延迟获取

隐式

使用 StatelessSession.fetch() 显式

JPQL 验证

运行时

编译时

此处的本质区别在于 Jakarta Data 不具有有状态持久性上下文。除其他影响外

  • 实体实例总是分离的,因此

  • 更新需要显式操作,并且

  • 没有透明的延迟关联获取。

重要的是要了解 Hibernate Data Repositories 中的存储库由 StatelessSession 支持,而不是由 Jakarta Persistence EntityManager 支持。

在 Jakarta Data 中获取关联的唯一便携方式就是通过 JPQL join fetch 子句,在 @Query 注解 中。该规范没有提供获取关联的延迟便携方式。要获取关联,我们需要 直接调用 StatelessSession。这确实没有听起来那么糟糕;由于与数据库服务器来回进行过多通信,因此过度使用延迟获取与性能不佳相关。

Jakarta Data 的未来版本将具有由 Jakarta Persistence 有状态持久性上下文支持的存储库,但此功能未针对 Jakarta Data 1.0 进行裁剪。

第二个重大区别在于 Jakarta Data 要求与数据库交互时使用专用于单个实体类型的用户编写方法,而不是像能够对所有实体类执行持久化操作的 EntityManager 那样提供通用接口。该方法标记了允许 Hibernate 填充方法实现的注解。

例如,虽然 Jakarta Persistence 定义了 EntityManager 的 find() 和 persist() 方法,但在 Jakarta Data 中,应用程序员需要编写如下接口

@Repository
interface Library {
    @Find
    Book book(String isbn);

    @Insert
    void add(Book book);
}

这是我们第一个存储库示例。

1.1. 存储库接口

存储库接口是由您(应用程序员)编写的并使用 @Repository 注解的接口。存储库接口的实现由 Jakarta Data 提供商提供,在我们的示例中,由 Hibernate Data Repositories 提供。

Jakarta Data 规范并未说明应如何执行此操作,但在 Hibernate Data Repositories 中,该实现由一个注解处理器生成。事实上,您可能已经在使用此注解处理器:它只是现已不恰当地命名为 hibernate-jpamodelgen 模块中的 HibernateProcessor

没错,我称之为 Hibernate Data Repositories 的花哨的东西实际上只是 Hibernate 值得尊重的静态元模型生成器的一种新功能。如果您已经在项目中使用 JPA 静态元模型,那么您就已经可以使用 Jakarta Data 了。如果您没有,我们将看到如何在 下一章 中进行设置。

当然,Jakarta Data 提供商无法生成任何任意方法的实现。因此,存储库接口的方法必须属于以下类别之一

  • default 方法,

  • 使用 @Insert@Update@Delete@Save 注解的 生命周期方法

  • 使用 @Find 注解的 自动查询方法

  • 使用 @Query@SQL 注解的 带注解查询方法

  • 资源访问器方法.

对于从 Spring Data 迁移过来的用户,Jakarta Data 还提供了一种名为按方法名查询的功能。对于新代码,我们不建议使用此方法,因为对于除最简单的示例之外的所有内容,它会产生冗长且不自然的名称。

我们很快会讨论每种方法。但首先我们需要提出一个更基本的问题:如何将持久化操作组织到存储库中,以及存储库接口与实体类型之间的关系是什么?也许令人惊讶的答案是:完全由您决定。

1.2. 组织持久性操作

Jakarta 数据允许你根据自己的喜好自由分配仓库中的持久性操作。特别是,Jakarta 数据不要求仓库界面集成声明基本“CRUD”操作的内置超类型,因此对于每个实体不必有单独的仓库界面。例如,你可以只使用单个 Library 界面,而不是 BookRepositoryAuthorRepositoryPublisherRepository

因此,整个编程模型比旧有方法(例如 Spring Data,它要求每个实体类有一个仓库界面,或至少对于所谓的“聚合”有)要灵活得多。

“聚合”概念在诸如文档数据库中是有意义的。但是关系数据没有聚合,你应该避免试图用这种不恰当的方式将关系表硬塞进正在考虑的数据中。

为了使用户(特别是从旧有框架迁移过来的用户)更方便,Jakarta Data 确实定义了 BasicRepositoryCrudRepository 界面,如果你喜欢的话可以使用它们。但在 Jakarta 数据中,这些界面不是特别重要的;使用相同的注释声明你自己的仓库的方法来声明其操作。以下示例说明了这种旧有、不灵活方法。

// old way

@Repository
interface BookRepository
        extends CrudRepository<Book,String> {
    // query methods
    ...
}

@Repository
interface AuthorRepository
        extends CrudRepository<Author,String> {
    // query methods
    ...
}

我们在这个文档中不会再看到 BasicRepositoryCrudRepository,因为它们不是必需的,并且因为它们实现了较旧、不灵活的方式。

相反,我们的仓库通常会将处理若干个相关实体的操作分组在一起,即使实体没有单个“根”。这种情况在关系数据模型中很常见。在我们的示例中,BookAuthor 通过 @ManyToMany 缔合关联,且都是“根”。

// new way

@Repository
interface Publishing {
    @Find
    Book book(String isbn);

    @Find
    Author author(String ssn);

    @Insert
    void publish(Book book);

    @Insert
    void create(Author author);

    // query methods
    ...
}

现在,让我们了解仓库界面可能会声明的不同类型的函数,从最简单的类型开始。如果以下总结不够充分,你可以在 Jakarta 数据规范的第 4 章和相关注释的 Javadoc 中找到有关仓库的更多详细信息。

1.3. 默认方法

default 方法是你自己实现的方法,并没有什么特别的。

@Repository
interface Library {
    default void hello() {
        System.out.println("Hello, World!");
    }
}

这看起来没什么用,至少不是在有某种方式从 default 方法与数据库交互时。为此,我们需要添加资源访问器方法。

1.4. 资源访问器方法

资源访问器方法是公开对基础实现类型的访问权限的方法。目前,Hibernate 数据仓库仅支持一种此类类型:StatelessSession。因此资源访问器方法只是任何返回 StatelessSession 的抽象方法。该方法的名称无关紧要。

StatelessSession session();

此方法返回支持仓库的 StatelessSession

通常,从同一仓库的 default 方法调用资源访问器方法。

default void refresh(Book book) {
    session().refresh(book);
}

这在我们需要直接访问 StatelessSession 以便利用 Hibernate 的全部功能时非常有用。

当我们需要延迟获取关联时,资源访问器方法也很有用。

library.session().fetch(book.authors);

当然,在通常情况下,我们希望 Jakarta Data 负责与 StatelessSession 进行交互。

1.5. 生命周期方法

Jakarta Data 1.0 定义了四个内置生命周期注释,这些注释与 Hibernate StatelessSession 的基本操作完全匹配。

  • @Insert 映射到 insert()

  • @Update 映射到 update()

  • @Delete 映射到 delete(),且

  • @Save 映射到 upsert()

StatelessSession 的基本操作(insert()update()delete()upsert())没有与之匹配的 CascadeType,因此,这些操作永远不会级联到关联的实体。

生命周期方法通常接受实体类型的一个实例,并且通常被声明为 void

@Insert
void add(Book book);

另外,它还可以接受一个实体列表或数组。(可变参数被视为一个数组。)

@Insert
void add(Book... books);

Jakarta Data 的未来版本可能会扩展内置生命周期注释的列表。具体而言,我们希望能够添加 @Persist@Merge@Refresh@Lock@Remove,以映射到 EntityManager 的基本操作。

如果仅仅能做到这样,存储库就毫无用处。当我们开始使用 Jakarta Data 来表示查询时,其优势才开始真正显现。

1.6. 自动查询方法

自动查询方法通常使用 @Find 进行注释。最简单的自动查询方法是通过唯一标识符检索实体实例的一个方法。

@Find
Book book(String isbn);

参数的名称表明这是一个通过主键进行查找(isbn 字段在 Book 中使用 @Id 进行注释),因此,这个方法将被实现为调用 StatelessSessionget() 方法。

如果参数名称与返回实体类型的任何字段不匹配,或者如果参数类型与匹配字段的类型不匹配,HibernateProcessor 会在编译时报告一个有用的错误。这是我们第一次了解到使用 Jakarta Data 存储库和 Hibernate 的好处。

如果数据库中没有具有给定 isbnBook,该方法将抛出 EmptyResultException。如果有两种方法可以解决这种情况

  • 声明方法以返回 Optional,或者

  • 使用 @jakarta.annotation.Nullable 对方法进行注释。

第一个选项得到规范的认可

@Find
Optional<Book> book(String isbn);

第二个选项是 Hibernate 提供的扩展

@Find @Nullable
Book book(String isbn);

自动查询方法可能会返回多个结果。在这种情况下,返回类型必须是实体类型的数组或列表。

@Find
List<Book> book(String title);

通常,自动查询方法的参数必须与实体的字段完全匹配。但是,Hibernate 提供了 @Pattern 注释,以允许使用 like 进行“模糊”匹配。

@Find
List<Book> books(@Pattern String title);

此外,如果参数类型是实体字段类型的列表或数组,则得到的查询有一个 in 条件。

@Find
List<Book> books(String[] ibsn);

当然,一个自动查询方法可能有多个参数。

@Find
List<Book> book(@Pattern String title, Year yearPublished);

这种情况下,每个参数都必须与实体的对应字段相匹配。

参数名中的字符 _ 可用来导航关联

@Find
List<Book> booksPublishedBy(String publisher_name);

然而,一旦查询开始涉及多个实体,通常最好使用 注释查询方法

@OrderBy 注释允许对结果排序。

@Find
@OrderBy("title")
@OrderBy("publisher.name")
List<Book> book(@Pattern String title, Year yearPublished);

这乍一看可能不怎么类型安全,但是——令人惊奇的是——@OrderBy 注释的内容在编译时会得到完全验证,如下所示。

自动查询方法对于非常简单的查询来说非常出色且方便。对于任何不太简单的查询,我们最好用 JPQL 编写查询。

1.7. 注解查询方法

注释查询方法使用以下方法声明

  • @Query 来自 Jakarta Data,或

  • @HQL@SQL 来自 org.hibernate.annotations.processing

定义 @Query 注释可以接受 JPQL、JDQL 或介于两者之间的任何内容。在 Hibernate Data Repositories 中,它接受任意的 HQL。

没有充分的理由不优先使用 @HQL 而不是 @Query。存在此注释是因为此处描述的功能早于 Jakarta Data 的存在。

考虑以下示例

@Query("where title like :pattern order by title, isbn")
List<Book> booksByTitle(String pattern);

您可能会注意到:

  • from 子句在 JDQL 中不需要,并且从仓库方法的返回类型推断出来。

  • 自 Jakarta Persistence 3.2 以来,JPQL 中也不需要 select 原因或实体别名(标识变量),最终将 HQL 的一个非常古老的功能标准化。

这样就可以以非常简洁的形式编写简单查询。

方法参数自动匹配查询的序数或命名参数。在前面的示例中,pattern 匹配 :pattern。在以下变体中,第一个方法参数匹配 ?1。

@Query("where title like ?1 order by title, isbn")
List<Book> booksByTitle(String pattern);

您可能想象 @Query 注释中指定的 JPQL 查询不能在编译时验证,但事实并非如此。HibernateProcessor 不仅能够验证查询的语法,它甚至能够完全对查询进行类型检查。这比将字符串传递给 EntityManagercreateQuery() 方法好得多,这可能是将 Jakarta Data 与 Hibernate 一起使用的首要原因。

当查询返回多个对象时,最好的做法是将每个结果打包为 Java record 类型的一个实例。例如,我们可能定义一个 record,其中包含 BookAuthor 的一些字段。

record AuthorBookSummary(String isbn, String ssn, String authorName, String title) {}

我们需要指定select子句中的值应打包为AuthorBookSummary的实例。JPQL 规范为此提供了select new构造。

@Query("select new AuthorBookRecord(b.isbn, a.ssn, a.name, b.title " +
       "from Author a join books b " +
       "where title like :pattern")
List<AuthorBookSummary> summariesForTitle(@Pattern String pattern);

请注意,此处需要from子句,因为无法从存储库方法的返回类型推断查询实体类型。

由于这非常冗长,Hibernate 不需要使用select new或别名,并且允许我们编写

@Query("select isbn, ssn, name, title " +
       "from Author join books " +
       "where title like :pattern")
List<AuthorBookSummary> summariesForTitle(@Pattern String pattern);

带注释的查询方法甚至可以执行updatedeleteinsert语句。

@Query("delete from Book " +
       "where extract(year from publicationDate) < :year")
int deleteOldBooks(int year);

该方法必须声明为void,或返回intlong。返回值是受影响记录的数量。

最后,可以使用@SQL指定本机 SQL 查询。

@SQL("select title from books where title like :pattern order by title, isbn")
List<String> booksByTitle(String pattern);

不幸的是,本机 SQL 查询无法在编译时验证,因此如果我们的 SQL 有任何问题,我们会在运行程序之前发现。

1.8. @By@Param

查询方法通过名称将方法参数与实体字段或查询参数进行匹配。有时,这会带来不便,导致方法参数名称不够自然。让我们重新考虑上面已经看到的示例

@Find
List<Book> books(String[] ibsn);

此处,因为参数名称必须与Bookisbn字段匹配,我们无法将其称为复数形式isbns

@By注释允许我们解决此问题

@Find
List<Book> books(@By("isbn") String[] ibsns);

当然,参数的名称和类型在编译时仍会被检查;尽管有字符串,但这里不会损失类型安全性。

@Param注释的作用要小得多,因为我们始终可以重命名 HQL 查询参数以匹配方法参数,或者在最坏的情况下,改用序数参数。

2. 配置和集成

开始使用 Hibernate 数据存储库涉及以下步骤

  1. 使用 Hibernate ORM 和HibernateProcessor设置项目,

  2. 配置持久单元,

  3. 确保可以注入该持久单元的StatelessSession,然后

  4. 使用 CDI 或jakarta.inject的其他一些实现注入存储库。

2.1. 项目设置

我们绝对需要在项目中具有以下依赖项

表 1. 所需依赖项
依赖项 说明

jakarta.data:jakarta.data-api

Jakarta 数据 API

org.hibernate.orm:hibernate-core

Hibernate ORM

org.hibernate.orm:hibernate-jpamodelgen

注释处理器本身

我们需要选择一个 JDBC 驱动程序

表 2. JDBC 驱动程序依赖项
数据库 驱动程序依赖项

PostgreSQL 或 CockroachDB

org.postgresql:postgresql

MySQL 或 TiDB

com.mysql:mysql-connector-j

MariaDB

org.mariadb.jdbc:mariadb-java-client

DB2

com.ibm.db2:jcc

SQL Server

com.microsoft.sqlserver:mssql-jdbc

Oracle

com.oracle.database.jdbc:ojdbc11

H2

com.h2database:h2

HSQLDB

org.hsqldb:hsqldb

此外,我们还可以向这个组合中添加一些以下选项。

表 3. 可选依赖关系
可选依赖项 说明

org.hibernate.validator:hibernate-validator
以及 org.glassfish:jakarta.el

Hibernate Validator

org.apache.logging.log4j:log4j-core

log4j

org.jboss.weld:weld-core-impl

Weld CDI

您需要配置注释处理器,以便在编译项目时运行。例如,在 Gradle 中,您需要使用 annotationProcessor

annotationProcessor 'org.hibernate.orm:hibernate-jpamodelgen:6.5.0'

2.2. 排除要从处理中排出的类

有三种方法可以将注释处理器限制到某些类中

  1. 通过指定 @Repository(provider="acme"),可以简单地将给定存储库排除在外,其中 "acme" 是任何字符串(除了空字符串或与 "Hibernate" 相等的字符串(忽略大小写))。当提供多个 Jakarta 数据提供程序时,这是首选的解决方案。

  2. 可以通过使用 org.hibernate.annotations.processing 中的 @Exclude 注释对其进行注释,来排除包或类型。

  3. 注释处理器可以限制为仅使用 include 配置选项考虑某些类型或某些包,例如,-Ainclude=*.entity.*,*Repository。或者,可以使用 exclude 选项排除类型或包,例如,-Aexclude=*Impl

2.3. 配置 Hibernate ORM

您配置 Hibernate 的方式取决于您运行的和偏好的环境

  • 在 Java SE 中,我们通常只使用 hibernate.properties,但有些人喜欢使用 persistence.xml,尤其是在有多个持久性单元的情况下,

  • 在 Quarkus 中,我们必须使用 application.properties,并且

  • 在 Jakarta EE 容器中,我们通常使用 persistence.xml

以下是用于 h2 数据库的简单 hibernate.properties 文件,仅供您参考。

# Database connection settings
jakarta.persistence.jdbc.url=jdbc:h2:~/h2temp;DB_CLOSE_DELAY=-1
jakarta.persistence.jdbc.user=sa
jakarta.persistence.jdbc.pass=

# Echo all executed SQL to console
hibernate.show_sql=true
hibernate.format_sql=true
hibernate.highlight_sql=true

# Automatically export the schema
hibernate.hbm2ddl.auto=create

请参阅 Hibernate 6 简介,以了解有关配置 Hibernate 的更多信息。

2.4. 获得一个 StatelessSession

每个存储库实现都必须通过某种方式为其持久性单元获取 StatelessSession

这是通过依赖注入进行的,因此您需要确保 StatelessSession 可用于注入

  • 在 Quarkus 中,已经帮我们解决了这个问题——为每个持久性单元始终都有一个可注入的 StatelessSession Bean,并且

  • 在 Jakarta EE 环境中, HibernateProcessor 会生成特殊代码来负责创建和销毁 StatelessSession,但是

  • 在其他环境中,我们需要自己处理这些问题。

根据生成路径中的库不同, HibernateProcessor 会生成不同的代码。例如,如果 Quarkus 在生成路径中,则存储库实现会被生成,以直接从 CDI 获取 StatelessSession,这种方式在 Quarkus 中有效,但在 WildFly 中无效。

如果您有多个持久性单元,则需要使用 @Repository(dataStore="my-persistence-unit-name") 为存储库接口消除持久性单元的歧义。

2.5. 注入一个存储库

原则上,可以使用 apache.jakarta.inject 的任何实现来注入存储库实现。

@Inject Library library;

但是,如果存储库实现无法从 Bean 容器中获取 StatelessSession,此代码将失败。

始终可以直接实例化存储库实现。

Library library = new Library_(statelessSession);

这对于测试或在不支持 apache.jakarta.inject 的环境中执行非常有用。

2.6. 与 Jakarta EE 集成

Jakarta Data 规定,存储库接口的方法可以注释为

  • Jakarta Bean 验证约束注解和

  • Jakarta 拦截器拦截绑定类型,包括

  • 特别是由 Jakarta Transactions 定义的 @Transactional 拦截绑定。

请注意,这些注解通常应用于 CDI Bean 实现类,而不是接口,[1] 但对于存储库接口则例外。

因此,在 Jakarta EE 环境或 Quarkus 中运行时,以及通过 CDI 获取存储库接口的实例时,将遵循此类注解的语义。

@Transactional @Repository
public interface Library {

    @Find
    Book book(@NotNull String isbn);

    @Find
    Book book(@NotBlank String title, @NotNull LocalDate publicationDate);

}

顺便说一句,看到所有这些功能如此完美地协作,令人相当满意,因为 Hibernate 团队的成员在持久性、Bean 验证、CDI、拦截器和数据规范的创建中发挥了关键作用。

3. 分页和动态排序

自动或标注文本查询方法可能具有附加参数,这些参数指定

  • 附加排序条件和/或

  • 限制返回给客户端的实际结果的限制和偏移。

在看到这些内容之前,先看看如何以完全类型安全的方式引用实体的字段。

3.1. 静态元模型

你可能已经熟悉 Jakarta Persistence 静态元模型。对于实体类 Book,类 Book_ 公开表示 Book 的持久字段的对象,例如,Book_.title 表示字段 title。此类在编译时由 HibernateProcessor 生成。

Jakarta Data 有其自己的静态元模型,它与 Jakarta Persistence 元模型不同,但在概念上非常相似。Jakarta Data 静态元模型针对 Book 不会使用 Book_,而是使用类 _Book 公开。

Jakarta Persistence 静态元模型通常与 Criteria Query API 或 EntityGraph 功能一起使用。即使这些 API 并非 Jakarta Data 编程模型的一部分,你仍然可以通过直接调用 StatelessSession,在存储库的 default 方法中使用它们。

让我们考虑一个简单的示例,看看如何使用静态元模型。

通过传递字段的名称,完全有可能获取 Sort 实例

var sort = Sort.asc("title");

遗憾的是,由于它在常规代码中,而不是注解中,因此字段名称 "title" 无法在编译时进行验证。

一个更好的解决方案是使用静态元模型来获取 Sort 实例。

var sort = _Book.title.asc();

静态元模型也声明包含持久字段名称的常量。例如,_Book.TITLE 计算为字符串 "title"

这些常量有时候用作注释值。

@Find
@OrderBy(_Book.TITLE)
@OrderBy(_Book.ISBN)
List<Book> books(@Pattern String title, Year yearPublished);

这个示例在表面上看起来更类型安全。但由于 Hibernate Data Repositories 已在编译时验证 @OrderBy 注释的内容,它实际上并没有更好。

3.2. 动态排序

使用类型 SortOrder 来表示动态排序条件

  • Sort 的实例表示排序查询结果的单个条件,且

  • Order 的实例打包多个 Sort

查询方法可能接受 Sort 实例。

@Find
List<Book> books(@Pattern String title, Year yearPublished,
                 Sort<Book> sort);

可以按如下方式调用此方法

var books =
        library.books(pattern, year,
                      _Book.title.ascIgnoreCase());

或者,该方法可能接受 Order 实例。

@Find
List<Book> books(@Pattern String title, Year yearPublished,
                 Order<Book> order);

现在可以按如下所示调用该方法

var books =
       library.books(pattern, year,
                     Order.of(_Book.title.ascIgnoreCase(),
                              _Book.isbn.asc());

动态排序条件可以与静态条件组合在一起。

@Find
@OrderBy("title")
List<Book> books(@Pattern String title, Year yearPublished,
                 Sort<Book> sort);

我们并不相信这在实践中非常有用。

3.3. 限制

Limit 是表示查询结果子范围的简单方式。它指定

  • maxResults,从数据库服务器返回给客户端的最大结果数,且

  • 另外,startAt,从第一个结果的偏移量。

这些值直接映射到 Jakarta Persistence Query 界面的熟悉的 setMaxResults()setFirstResults()

@Find
@OrderBy(_Book.TITLE)
List<Book> books(@Pattern String title, Year yearPublished,
                 Limit limit);
var books =
        library.books(pattern, year,
                      Limit.of(MAX_RESULTS));

PageRequest 提供了一种更复杂的方法。

3.4. 基于偏移的分页

PageRequest 在表面上与 Limit 类似,但它使用以下条件指定

  • size,且

  • 编号 page

我们可以像使用 Limit 一样使用 PageRequest

@Find
@OrderBy("title")
@OrderBy("isbn")
List<Book> books(@Pattern String title, Year yearPublished,
                 PageRequest pageRequest);
var books =
        library.books(pattern, year,
                      PageRequest.ofSize(PAGE_SIZE));

当将资源库方法用于分页时,查询结果应完全有序。确保有良好定义的总体顺序的最简单方法是指定实体的标识符作为顺序的最后一个元素。出于此原因,我们在前一个示例中指定了 @OrderBy("isbn")

但是,接受 PageRequest 的资源库方法可能会返回 Page 而不是 List,从而更轻松地实现分页。

@Find
@OrderBy("title")
@OrderBy("isbn")
Page<Book> books(@Pattern String title, Year yearPublished,
                 PageRequest pageRequest);
var page =
        library.books(pattern, year,
                      PageRequest.ofSize(PAGE_SIZE));
var books = page.content();
long totalPages = page.totalPages();
// ...
while (page.hasNext()) {
    page = library.books(pattern, year,
                         page.nextPageRequest().withoutTotal());
    books = page.content();
    // ...
}

分页可以与动态排序结合在一起。

@Find
Page<Book> books(@Pattern String title, Year yearPublished,
                 PageRequest pageRequest, Order<Book> order);

在每个页面请求中,将相同参数传递给查询参数并使用相同的排序条件非常重要!资源库是无状态的:它不记得前一页请求中传递的值。

返回类型为 Page 的存储库方法使用 SQL 偏移量和限制来实现分页。我们将此称为基于偏移的分页。此方法存在的问题在于,当在页面请求之间修改数据库时,很容易错过或重复结果。因此,Jakarta Data 提供了一个替代方案,我们称之为基于密钥的分页

3.5. 基于键的分页

在基于密钥的分页中,查询结果必须按照结果集的唯一密钥完全排序。SQL 偏移将替换为对唯一密钥的限制,附加到查询的 where 子句

  • 请求查询结果的下一页将使用当前页上最后一个结果的键值来限制结果,或者

  • 请求查询结果的上一页将使用当前页上第一个结果的键值来限制结果。

对于基于密钥的分页,查询必须具有完全顺序,这一点至关重要

从我们作为 Jakarta Data 用户的角度来看,基于密钥的分页的工作方式几乎与基于偏移的分页完全相同。不同之处在于,我们必须声明我们的存储库方法返回 CursoredPage

@Find
@OrderBy("title")
@OrderBy("isbn")
CursoredPage<Book> books(@Pattern String title, Year yearPublished,
                 PageRequest pageRequest);

另一方面,对于基于密钥的分页,Hibernate 必须在后台做一些工作,改写我们的查询。

基于密钥的分页在一定程度上可以保护我们免受跳过或重复结果的影响。代价是,在导航过程中页面编号可能会与查询结果集失去同步。这通常并不是问题,但我们需要注意这一点。

对基于密钥的分页的直接 API 支持源于 2015 年 Hibernate 团队成员 Christian Beikov 在 Blaze-Persistence 框架中开展的工作。它由此被 Jakarta Data 规范采用,现在甚至可以通过 KeyedPage/KeyedResultList API 在 Hibernate ORM 中使用。

3.6. 对查询的深入控制

对于更高级的用法,可声明自动或带注释的查询方法返回 jakarta.persistence.Queryjakarta.persistence.TypedQueryorg.hibernate.query.Queryorg.hibernate.query.SelectionQuery

@Find
SelectionQuery<Book> booksQuery(@Pattern String title, Year yearPublished);

default List<Book> booksQuery(String title, Year yearPublished) {
    return books(title, yearPublished)
            .enableFetchProfile(_Book.PROFILE_WITH_AUTHORS);
            .setReadOnly(true)
            .setTimeout(QUERY_TIMEOUT)
            .getResultList();
}

这样可以对查询执行进行直接控制,而不会失去类型安全。


1@Inherited 注释从超类继承到子类,但不从接口继承到实现类。