Hibernate ORM 中文操作指南

6. Compile-time tooling

元模型生成器是 JPA 的标准部分。我们实际上已经在代码示例 earlier中看到了它的杰作:它是类_Book__的作者,其中包含 entity class_Book_的静态元模型。

我们在之前看到的 Gradle build 中已经介绍过如何设置注释处理器。有关如何集成元模型生成器的更多详细信息,请查看用户指南中的 Static Metamodel Generator 部分。

以下是一个实体类的生成代码示例,由 JPA 规范规定:

  1. 生成的代码

@StaticMetamodel(Book.class)
public abstract class Book_ {

    /**
     * @see org.example.Book#isbn
     **/
    public static volatile SingularAttribute<Book, String> isbn;

    /**
     * @see org.example.Book#text
     **/
    public static volatile SingularAttribute<Book, String> text;

    /**
     * @see org.example.Book#title
     **/
    public static volatile SingularAttribute<Book, String> title;

    /**
     * @see org.example.Book#type
     **/
    public static volatile SingularAttribute<Book, Type> type;

    /**
     * @see org.example.Book#publicationDate
     **/
    public static volatile SingularAttribute<Book, LocalDate> publicationDate;

    /**
     * @see org.example.Book#publisher
     **/
    public static volatile SingularAttribute<Book, Publisher> publisher;

    /**
     * @see org.example.Book#authors
     **/
    public static volatile SetAttribute<Book, Author> authors;

    public static final String ISBN = "isbn";
    public static final String TEXT = "text";
    public static final String TITLE = "title";
    public static final String TYPE = "type";
    public static final String PUBLICATION_DATE = "publicationDate";
    public static final String PUBLISHER = "publisher";
    public static final String AUTHORS = "authors";

}

对于实体的每个属性,Book_ 类都有:

  • 一个 String 值常量,如 TITLE ,和

  • 一个类型安全的引用,如 title 到类型 Attribute 的元模型对象。

我们已经在前面的章节中使用过 Book.authors_ 和 Book.AUTHORS 等元模型引用。所以现在让我们看看元模型生成器还能为我们做什么。

元模型生成器提供 statically-typed 访问 JPA 的元素 Metamodel 。但 Metamodel 也可通过 EntityManagerFactory 以“反射”方式进行访问。

EntityType<Book> book = entityManagerFactory.getMetamodel().entity(Book.class); SingularAttribute<Book,Long> id = book.getDeclaredId(Long.class) EntityType<Book> book = entityManagerFactory.getMetamodel().entity(Book.class); SingularAttribute<Book,Long> id = book.getDeclaredId(Long.class) 这对于在框架或库中编写通用代码非常有用。例如,您可以使用它来创建自己的条件查询 API。

自动生成 finder methodsquery methods 是 Hibernate 元模型生成器实现的一项新功能,以及对 JPA 规范所定义的功能进行的扩展。在本章中,我们将探讨这些功能。

本章的其余部分中描述的功能依赖于 Entities 中描述的注释的使用。目前元模型生成器无法为完全在 XML 中声明的实体生成查找器方法和查询方法,它也无法验证查询此类实体的 HQL。(另一方面, O/R mappings 可以用 XML 指定,因为元模型生成器不需要它们。)

我们将遇到三种不同的生成方法:

  1. a _ named query method_ 的签名及其实现直接从一个 @NamedQuery 注解中生成,

  2. a _ query method_ 有一个明确声明的签名和一个通过 @HQL@SQL 注解指定的 HQL 或 SQL 查询执行的生成实现,以及

  3. 一个 _ finder method_ 注解的 @Find 有一个明确声明的签名和一个从参数列表推断出的生成实现。

我们还将了解可以调用这些方法的两种方式:

  1. 作为某个生成的抽象类的静态方法,或

  2. 作为 instance methods of an interface,具有一个甚至可能为 injected 的生成实现。

为了激发我们的兴趣,我们来看看这如何适用于 @NamedQuery

6.1. Named queries and the Metamodel Generator

生成查询方法最简单的方法是将 @NamedQuery 注释放在我们喜欢的任何地方,其中 name 以神奇字符 # 开头。

让我们把它贴在 Book 类上:

@CheckHQL // validate the query at compile time
@NamedQuery(name = "#findByTitleAndType",
            query = "select book from Book book where book.title like :title and book.type = :type")
@Entity
public class Book { ... }

现在,元模型生成器向元模型类 Book_ 添加了以下方法声明。

  1. 生成的代码

/**
 * Execute named query {@value #QUERY_FIND_BY_TITLE_AND_TYPE} defined by annotation of {@link Book}.
 **/
public static List<Book> findByTitleAndType(@Nonnull EntityManager entityManager, String title, Type type) {
    return entityManager.createNamedQuery(QUERY_FIND_BY_TITLE_AND_TYPE)
            .setParameter("title", title)
            .setParameter("type", type)
            .getResultList();
}

我们可以轻松地从任何我们喜欢的地方调用此方法,只要我们可以访问 EntityManager

List<Book> books =
        Book_.findByTitleAndType(entityManager, titlePattern, Type.BOOK);

现在,这确实很好,但它在各个方面都有点儿不灵活,所以这很可能 isn’t 是生成查询方法的最佳方式。

6.2. Generated query methods

直接从 @NamedQuery 注释生成查询方法的主要问题在于它不允许我们明确指定返回类型或参数列表。在我们就看到的案例中,元模型生成器在推断查询返回类型和参数类型方面做得不错,但是我们常常需要更多的控制。

解决方案是将查询方法 explicitly 的签名写为 Java 中的抽象方法。我们需要一个放置此方法的地方,由于我们的 Book 实体不是抽象类,我们将为此目的引入一个新接口:

interface Queries {
    @HQL("where title like :title and type = :type")
    List<Book> findBooksByTitleAndType(String title, String type);
}

使用新的 @HQL 注释(我们将其直接放在查询方法上)指定 HQL 查询,而不是作为类型级别注释的 @NamedQuery。这将在 Queries_ 类中生成以下代码:

  1. 生成的代码

@StaticMetamodel(Queries.class)
public abstract class Queries_ {

    /**
     * Execute the query {@value #FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type}.
     *
     * @see org.example.Queries#findBooksByTitleAndType(String,Type)
     **/
    public static List<Book> findBooksByTitleAndType(@Nonnull EntityManager entityManager, String title, Type type) {
        return entityManager.createQuery(FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type, Book.class)
                .setParameter("title", title)
                .setParameter("type", type)
                .getResultList();
    }

    static final String FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type =
            "where title like :title and type = :type";

}

请注意,签名与我们在 Queries 接口中写下的签名略有不同:元模型生成器在前置了接受 EntityManager 的参数,并将其添加到了参数列表中。

如果我们想要明确指定此参数的名称和类型,我们可能明确声明它:

interface Queries {
    @HQL("where title like :title and type = :type")
    List<Book> findBooksByTitleAndType(StatelessSession session, String title, String type);
}

Metamodel 生成器默认使用 EntityManager 作为会话类型,但允许使用其他类型:

  1. Session,

  2. StatelessSession, or

  3. Mutiny.Session from Hibernate Reactive.

这一切的真正价值在于可以在编译时执行的检查中。Metamodel 生成器验证我们抽象方法声明的参数是否与 HQL 查询的参数匹配,例如:

  1. 对于一个命名参数 :alice,必须有一个名为 alice 且类型完全相同的某个方法参数,或

  2. 对于一个序数参数 ?2,第二个方法参数必须具有完全相同的类型。

查询还必须在语法上合法且在语义上类型良好,也就是说,查询中引用的实体、属性和函数必须实际存在并且具有兼容的类型。Metamodel 生成器通过在编译时检查实体类的注释来确定这一点。

指导 Hibernate 验证命名查询的 @CheckHQL 注释对于带 @HQL 注释的查询方法是 not 必要的。

@HQL 注释有一个名为 @SQL 的朋友,它允许我们指定用原生 SQL 编写的查询,而不是用 HQL 编写的查询。在这种情况下,元模型生成器可以对查询进行合法性和类型良好检查。

我们想象你可能会疑惑,一个 static 方法是否真的是这里使用的方法。

6.3. Generating query methods as instance methods

我们刚刚看到的这部分内容中有一点不太理想,那就是我们不能透明地替换一个生成的 static 函数,将它替换为改进后的手写实现,而不影响客户端。现在,如果我们的查询只在一个地方被调用,这是很常见的情况,这不会成为一个大问题,因此我们倾向于认为 static 函数是没有问题的。

但是,如果这个函数在许多地方都被调用,那么最好将它提升到某个类或接口的实例方法。幸运的是,这很简单。

我们所需要做的就是为我们 Queries 接口添加一个会话对象的抽象 getter 方法。(并且从方法参数列表中移除会话。)我们可以随心所欲地调用此方法:

interface Queries {
    EntityManager entityManager();

    @HQL("where title like :title and type = :type")
    List<Book> findBooksByTitleAndType(String title, String type);
}

在这里,我们使用了 EntityManager 作为会话类型,但正如我们在上面看到的,允许使用其他类型。

现在,Metamodel 生成器的做法稍有不同:

  1. 生成的代码

@StaticMetamodel(Queries.class)
public class Queries_ implements Queries {

    private final @Nonnull EntityManager entityManager;

    public Queries_(@Nonnull EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public @Nonnull EntityManager entityManager() {
        return entityManager;
    }

    /**
     * Execute the query {@value #FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type}.
     *
     * @see org.example.Queries#findBooksByTitleAndType(String,Type)
     **/
    @Override
    public List<Book> findBooksByTitleAndType(String title, Type type) {
        return entityManager.createQuery(FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type, Book.class)
                .setParameter("title", title)
                .setParameter("type", type)
                .getResultList();
    }

    static final String FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type =
            "where title like :title and type = :type";

}

生成的类 Queries_ 现在实现了 Queries 接口,生成的查询方法直接实现了我们抽象方法。

当然,调用查询方法的协议必须改变:

Queries queries = new Queries_(entityManager);
List<Book> books = queries.findByTitleAndType(titlePattern, Type.BOOK);

如果我们曾经需要用一个手动编写的查询方法来替换生成的查询方法,而不影响客户端,那么我们所需要做的就是用 default 方法替换 Queries 接口的抽象方法。例如:

interface Queries {
    EntityManager entityManager();

    // handwritten method replacing previous generated implementation
    default List<Book> findBooksByTitleAndType(String title, String type) {
        entityManager()
                .createQuery("where title like :title and type = :type", Book.class)
                        .setParameter("title", title)
                        .setParameter("type", type)
                        .setFlushMode(COMMIT)
                        .setMaxResults(100)
                        .getResultList();
    }
}

如果我们想要注入 Queries 对象,而不是直接调用它的构造函数怎么办?

正如你 recall 的所想,我们认为这些内容并非一定要成为容器管理对象。但是如果你 want 它们——如果你出于某种原因而不愿意调用这些构造函数,那么:

在构建路径上放置 jakarta.inject 会导致在 Queries_ 的构造函数中添加一个 @Inject 注解,并且

在构建路径上放置 jakarta.enterprise.context 会导致在 Queries_ 类中添加一个 @Dependent 注解。

因此, Queries 生成的实现将是一个功能完全正常的 CDI bean,无需执行其他操作。

Queries 接口是否开始看起来更像 DAO 风格的存储库对象?嗯,也许。你当然可以 decide to use 这个设施去创建一个 BookRepository,如果你愿意这么做的话。但与存储库不同,我们的 Queries 接口:

  1. 不会尝试向其客户端隐藏 EntityManager

  2. 至少不想要自己创建这样的框架,否则

  3. 不限于服务某个特定的实体类。

我们可以随意使用任意数量的带有查询方法的接口。这些接口和实体类型之间没有一对一对应关系。这种方法非常灵活,我们甚至不知道如何称呼这些“带有查询方法的接口”。

6.4. Generated finder methods

在这一点上,人们通常开始质疑是否还有必要编写查询。是否可能仅从方法签名推断查询?

在一些简单的情况下,这实际上是可能的,这就是 finder methods 的目的。Finder 方法是一个带有 @Find 注解的方法。例如:

@Find
Book getBook(String isbn);

Finder 方法可能有多个参数:

@Find
List<Book> getBooksByTitle(String title, Type type);

查找器方法的名称是随意的,不带任何语义。但是:

  1. 返回类型确定要查询的实体类,且

  2. 该方法的参数必须与实体类的字段匹配 exactly,包括名称和类型。

考虑到第一个示例,Book 具有一持久域 String isbn,因此此查找器方法是合法的。如果 Book 中没有名为 isbn 的域,或其具有不同类型,则此方法声明将被拒绝,并在编译时出现有意义的错误。类似地,第二个示例是合法的,因为 Book 具有域 String titleType type

你可能会注意到,我们针对此问题的解决方案与其他人采取的方法非常不同。在 DAO 样式的存储库框架中,系统会要求你将查找器方法的语义编码到 name of the method 中。这个想法来自 Ruby 传到 Java,我们认为它不属于这里。它在 Java 中是完全不自然的,并且几乎在所有方面(除了 counting characters)都比在字符串文字中编写查询差。至少字符串文字可以容纳空格和标点符号。哦,你知道吗,能够重命名查找器方法 without changing its semantics 非常有用。🙄

为该查找器方法生成的代码取决于与方法参数匹配的字段类型:

@Id field

Uses EntityManager.find()

All @NaturalId fields

Uses Session.byNaturalId()

其他持久字段,或字段类型的混合

Uses a criteria query

生成的代码还取决于我们拥有的会话类型,因为无状态会话和响应式会话的功能与常规状态会话的功能略有不同。

EntityManager 作为会话类型,我们将获得:

/**
 * Find {@link Book} by {@link Book#isbn isbn}.
 *
 * @see org.example.Dao#getBook(String)
 **/
@Override
public Book getBook(@Nonnull String isbn) {
	return entityManager.find(Book.class, isbn);
}

/**
 * Find {@link Book} by {@link Book#title title} and {@link Book#type type}.
 *
 * @see org.example.Dao#getBooksByTitle(String,Type)
 **/
@Override
public List<Book> getBooksByTitle(String title, Type type) {
	var builder = entityManager.getEntityManagerFactory().getCriteriaBuilder();
	var query = builder.createQuery(Book.class);
	var entity = query.from(Book.class);
	query.where(
			title==null
				? entity.get(Book_.title).isNull()
				: builder.equal(entity.get(Book_.title), title),
			type==null
				? entity.get(Book_.type).isNull()
				: builder.equal(entity.get(Book_.type), type)
	);
	return entityManager.createQuery(query).getResultList();
}

甚至可以将查找器方法的参数与关联实体或可嵌入实体的属性进行匹配。自然语法将是 String publisher.name 这样的参数声明,但由于这在 Java 中是非法的,因此我们可以将其写为 String publisher$name,利用一个合法的 Java 标识符字符,而其他人永远不会将其用于其他任何用途:

@Find
List<Book> getBooksByPublisherName(String publisher$name);

可以将 @Pattern 注释应用于类型为 String 的参数,表示参数是通配模式,将使用 like 进行比较。

@Find
List<Book> getBooksByTitle(@Pattern String title, Type type);

查找器方法可以指定 fetch profiles,例如:

@Find(namedFetchProfiles=Book_.FETCH_WITH_AUTHORS)
Book getBookWithAuthors(String isbn);

这使我们可以声明应该通过注释 Book 类来预先获取 Book 的哪些关联。

6.5. Paging and ordering

或者,查询方法(或返回多个结果的查找器方法)可能具有不映射到查询参数的其他“神奇”参数:

Parameter type

Purpose

Example argument

Page

指定查询结果的一页

Page.first(20)

Order<? super E>

指定要按其排序的实体属性,如果 E 是查询返回的实体类型

Order.asc(Book.title)_

List&lt;Order? super E&gt; (或 varargs)

指定要按其排序的实体属性,如果 E 是查询返回的实体类型

List.of(Order.asc(Book.title), Order.asc(Book_.isbn))_

Order<Object[]>

指定要按其排序的列,如果查询返回投影列表

Order.asc(1)

List<Object[]> (or varargs)

指定要按其排序的列,如果查询返回投影列表

List.of(Order.asc(1), Order.desc(2))

因此,如果我们将较早的查询方法重新定义为:

interface Queries {
    @HQL("from Book where title like :title and type = :type")
    List<Book> findBooksByTitleAndType(String title, Type type,
                                       Page page, Order<? super Book>... order);
}

那么我们可以这样调用它:

List<Book> books =
        Queries_.findBooksByTitleAndType(entityManager, titlePattern, Type.BOOK,
                Page.page(RESULTS_PER_PAGE, page), Order.asc(Book_.isbn));

或者,我们可以将此查询方法编写为查找器方法:

interface Queries {
    @Find
    List<Book> getBooksByTitle(String title, Type type,
                               Page page, Order<? super Book>... order);
}

这会对查询执行提供一些动态控制,但是如果我们希望对 Query 对象进行直接控制,该怎么办?嗯,让我们讨论一下返回类型。

6.6. Key-based pagination

生成的查询或查找器方法可以使用 key-based pagination

@Query("where publicationDate > :minDate")
KeyedResultList<Book> booksFromDate(Session session, LocalDate minDate, KeyedPage<Book> page);

请注意此方法:

  1. accepts a KeyedPage, and

  2. returns KeyedResultList.

可以这样使用这样的方法:

// obtain the first page of results
KeyedResultList<Book> first =
        Queries_.booksFromDate(session, minDate,
                Page.first(25).keyedBy(Order.asc(Book_.isbn)));
List<Book> firstPage = first.getResultList();
...

if (!firstPage.isLastPage()) {
    // obtain the second page of results
    KeyedResultList<Book> second =
            Queries_.booksFromDate(session, minDate,
                    firstPage.getNextPage());
    List<Book> secondPage = second.getResultList();
    ...
}

6.7. Query and finder method return types

查询方法无需返回 List。它可能返回单个 Book

@HQL("where isbn = :isbn")
Book findBookByIsbn(String isbn);

对于带有投影列表的查询,Object[]List<Object[]> 是允许的:

@HQL("select isbn, title from Book where isbn = :isbn")
Object[] findBookAttributesByIsbn(String isbn);

但是当 select 列表中只有一个项目时,应该使用该项目的类型:

@HQL("select title from Book where isbn = :isbn")
String getBookTitleByIsbn(String isbn);
@HQL("select local datetime")
LocalDateTime getServerDateTime();

返回选择列表的查询可能有一个查询方法,该方法重新封装结果为记录,就像我们在 Representing projection lists 中看到的。

record IsbnTitle(String isbn, String title) {}

@HQL("select isbn, title from Book")
List<IsbnTitle> listIsbnAndTitleForEachBook(Page page);

查询方法甚至可以返回 TypedQuerySelectionQuery:

@HQL("where title like :title")
SelectionQuery<Book> findBooksByTitle(String title);

这有时非常有用,因为它允许客户端进一步操作查询:

List<Book> books =
        Queries_.findBooksByTitle(entityManager, titlePattern)
            .setOrder(Order.asc(Book_.title))                   // order the results
            .setPage(Page.page(RESULTS_PER_PAGE, page))         // return the given page of results
            .setFlushMode(FlushModeType.COMMIT)                 // don't flush session before query execution
            .setReadOnly(true)                                  // load the entities in read-only mode
            .setCacheStoreMode(CacheStoreMode.BYPASS)           // don't cache the results
            .setComment("Hello world!")                         // add a comment to the generated SQL
            .getResultList();

insertupdatedelete 查询必须返回 intbooleanvoid

@HQL("delete from Book")
int deleteAllBooks();
@HQL("update Book set discontinued = true where discontinued = false and isbn = :isbn")
boolean discontinueBook(String isbn);
@HQL("update Book set discontinued = true where isbn = :isbn")
void discontinueBook(String isbn);

另一方面,查找方法当前的限制更大。查找方法必须返回实体类型(如 Book)或实体类型列表(如 List<Book>)。

如你所料,对于反应式会话,所有查询方法和查找器方法都必须返回 Uni

6.8. An alternative approach

如果你不喜欢我们在本章中提出的想法,而是希望直接调用 SessionEntityManager,但仍然需要对 HQL 进行编译时验证,该怎么办?或者,如果你 do 喜欢这些想法,但正在处理一个庞大的现有代码库,其中包含你不想更改的代码,该怎么办?

那么,也有一个可供你使用的解决方案。 Query Validator 是一款单独的注释处理器,它能够类型检查 HQL 字符串,不仅存在于注释中,甚至当它们作为 createQuery()createSelectionQuery()_或 _createMutationQuery() 的参数出现时也是如此。它甚至能够检查对 setParameter() 的调用(有一定限制)。

Query Validator 可在 javac、Gradle、Maven 和 Eclipse Java 编译器中运行。

与元模型生成器(这是一个完全基于仅标准 Java API 的标准 Java 注释处理器)不同,查询验证器使用 javacecj 中的内部编译器 API。这意味着不能保证它能在所有 Java 编译器中都能正常工作。已知当前版本在 JDK 11 及更高版本中可用,但建议使用 JDK 15 或更高版本。