Hibernate 6 中文文档(六)(版本6.3.2.Final)

   译自:An Introduction to Hibernate 6

系列文章:

Hibernate 6 中文文档(一)

Hibernate 6 中文文档(二)

Hibernate 6 中文文档(三)

Hibernate 6 中文文档(四)

Hibernate 6 中文文档(五)

Hibernate 6 中文文档(六)

目录

6. 编译时工具

6.1. 命名查询和元模型生成器

6.2. 生成查询方法

6.3 生成实例方法的查询方法

6.4 生成查找方法 (Finder Methods)

6.5 分页和排序 (Paging and Ordering)

6.6 查询和查找方法的返回类型


6. 编译时工具

元模型生成器 是 JPA 的标准组件。在前面的代码示例中,我们已经见识过它的作用:它生成了 Book_ 类,该类包含实体类 Book 的静态元模型。

元模型生成器


        Hibernate 的元模型生成器是一种注解处理器,它生成 JPA 所称的静态元模型(static metamodel)。简单来说,它会为程序中的持久化类生成一个类型化的模型,为我们在 Java 代码中以类型安全的方式引用这些类的属性提供支持。尤其是,它允许我们以完全类型安全的方式指定实体图(Entity Graphs)和条件查询(Criteria Queries)。

这个工具的历史颇为有趣。在 Java 的注解处理 API 刚刚推出时,静态元模型的概念由 Gavin King 提出,并在 JPA 2.0 中得以实现,目的是为新生的条件查询 API 提供类型安全的支持。然而在 2010 年,这个 API 并未取得立竿见影的成功。那时的工具并不支持注解处理器,显式泛型类型也让代码显得冗长且难以阅读。(此外,显式引用 CriteriaBuilder 实例的需求也为条件查询 API 增加了更多冗长代码。)多年后,Gavin 将其视为自己较为尴尬的一个失误。

        然而,时间对静态元模型是友好的。到 2023 年,所有 Java 编译器、构建工具和 IDE 都已全面支持注解处理,而 Java 的局部类型推断(var 关键字)也消除了冗长的泛型类型。尽管 JPA 的 CriteriaBuilder 和 EntityGraph API 仍不算完美,但这些不足与静态类型安全或注解处理并无直接关系。静态元模型本身已被证明是非常有用且优雅的。

因此,现在 Hibernate 6.3 已准备好在元模型生成器的基础上进一步探索。事实证明,这个工具还有很多潜在的能力尚未被充分利用。

需要注意的是,使用 Hibernate 时,元模型生成器并非必需——前述 API 仍支持普通字符串作为输入。不过,我们发现元模型生成器与 Gradle 搭配良好,并能够与 IDE 无缝集成,同时它在类型安全方面的优势非常吸引人。

在前面我们已经展示了如何在 Gradle 构建中设置注解处理器。如需更多关于如何集成元模型生成器的详细信息,请查阅《用户指南》中的静态元模型生成器部分。

以下是 JPA 规范要求为实体类生成的代码示例:

@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_ 类包含:

  1. 一个值为字符串的常量,例如 TITLE
  2. 一个类型安全的引用,例如 title,它是一个 Attribute 类型的元模型对象。

在前几章中,我们已经使用了类似 Book_.authorsBook.AUTHORS 的元模型引用。现在,让我们看看元模型生成器还能为我们做些什么。

元模型生成器的功能

元模型生成器为 JPA 元模型的元素提供了静态类型的访问方式。但通过 EntityManagerFactory,元模型也可以以“反射式”的方式访问。例如:

EntityType<Book> book = entityManagerFactory.getMetamodel().entity(Book.class);
SingularAttribute<Book, Long> id = book.getDeclaredId(Long.class);

这种方式对于在框架或库中编写通用代码非常有用。例如,您可以利用它创建自己的条件查询 API。

Hibernate 的元模型生成器新增了自动生成 Finder 方法和查询方法的功能,这是对 JPA 规范功能的扩展。在本章中,我们将探索这些特性。

功能依赖

本章接下来的功能描述依赖于实体中使用的注解(详见实体章节)。目前,元模型生成器无法为完全通过 XML 声明的实体生成 Finder 方法和查询方法,也无法验证查询这些实体的 HQL。(另一方面,O/R 映射可以通过 XML 指定,因为元模型生成器并不需要它们。)

我们将介绍三种不同类型的生成方法:

  1. 命名查询方法:根据 @NamedQuery 注解直接生成方法签名和实现。
  2. 查询方法:明确声明方法签名,并生成一个实现,该实现会执行通过 @HQL@SQL 注解指定的 HQL 或 SQL 查询。
  3. Finder 方法:使用 @Find 注解的方法,明确声明方法签名,并根据参数列表推断生成实现。

为了让大家先睹为快,让我们看看如何使用 @NamedQuery 来实现这些功能。

6.1. 命名查询和元模型生成器

生成查询方法的最简单方法是在任意位置使用 @NamedQuery 注解,并以“#”开头为其命名。例如,将它放在 Book 类中:

@CheckHQL // 在编译时验证查询语法
@NamedQuery(name = "#findByTitleAndType",
            query = "select book from Book book where book.title like :titlen and book.type = :type")
@Entity
public class Book { ... }

元模型生成器会在元模型类 Book_ 中添加以下方法声明:

/**
 * 执行由 {@link Book} 的注解定义的命名查询 {@value #QUERY_FIND_BY_TITLE_AND_TYPE}。
 **/
public static List<Book> findByTitleAndType(@Nonnull EntityManager entityManager, String title, Type type) {
    return entityManager.createNamedQuery(QUERY_FIND_BY_TITLE_AND_TYPE)
            .setParameter("titlePattern", title)
            .setParameter("type", type)
            .getResultList();
}

我们可以在任意有 EntityManager 的地方调用这个方法,例如:

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

这种方法虽然简单,但在某些方面缺乏灵活性,因此可能并不是生成查询方法的最佳方式。

6.2. 生成查询方法

@NamedQuery 注解直接生成查询方法的主要问题是,它无法让我们显式地指定返回类型或参数列表。在上面的例子中,元模型生成器可以合理地推断出返回类型和参数类型,但我们往往需要更高的控制力。

解决方案:显式声明查询方法签名

我们可以将查询方法作为一个抽象方法明确地写在 Java 中。由于 Book 实体不是抽象类,我们可以为此引入一个新的接口:

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

这里,我们使用了新的 @HQL 注解(替代 @NamedQuery 这种类级别注解),直接将 HQL 查询放置在查询方法上。这会生成以下代码到 Queries_ 类中:

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

    /**
     * 执行查询 {@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 参数。

显式声明 EntityManager 参数

如果我们希望明确指定这个参数的名称和类型,可以显式声明它:

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

元模型生成器默认使用 EntityManager 作为会话类型,但我们也可以使用其他类型:

  • Session
  • StatelessSession
  • Mutiny.Session(用于 Hibernate Reactive)

 译者注:

EntityManagerSessionStatelessSessionMutiny.Session 是 Hibernate 中不同的会话接口,它们都可以被元模型生成器用作会话类型,但每种类型的功能和适用场景各有不同。以下是它们的区别和为什么可以替代的详细解析:


1. EntityManager

EntityManager 是 JPA 的标准接口,用于管理持久化上下文,与数据库进行交互。

特点

  • 一级缓存:维护一个持久化上下文,缓存当前事务中的实体。
  • 自动脏检查:在事务提交或刷新时,自动检测实体的变化并同步到数据库。
  • 标准化:作为 JPA 的一部分,具有良好的可移植性。
  • 支持 JPA 特性:如 mergedetachpersist 等操作。

适用场景

  • 适合标准 JPA 应用,特别是复杂的实体关系管理。
  • 涉及延迟加载或依赖一级缓存的操作。


2. Session

Session 是 Hibernate 的核心接口,与 EntityManager 类似,但它是 Hibernate 原生的实现。

特点

  • 更灵活:支持 Hibernate 特有的功能,如 getCurrentSession() 和更强大的批量操作支持。
  • 更贴近数据库:与底层数据库的交互更加直接。
  • 对 JPA 的支持:尽管是 Hibernate 原生接口,但可以与 JPA 特性配合使用。

适用场景

  • 使用 Hibernate 特性的原生场景。
  • 需要直接访问底层数据库功能的操作。

与 EntityManager 的区别

  • Session 是 Hibernate 特有的,提供了更多特定于 Hibernate 的功能。
  • EntityManager 是 JPA 标准化的接口,更具通用性。


3. StatelessSession

StatelessSession 是 Hibernate 提供的无状态会话接口,用于高性能批量操作或简单查询。

特点

  • 没有一级缓存:不会维护持久化上下文,所有操作直接与数据库交互。
  • 无自动脏检查:所有更改需要显式提交。
  • 高性能:适合大批量数据的插入、更新或删除操作,开销较低。

适用场景

  • 批量插入、更新或删除操作。
  • 不需要缓存或上下文管理的只读查询。
  • 性能敏感的场景,例如处理海量数据时。

4. Mutiny.Session

Mutiny.Session 是 Hibernate Reactive 的核心接口,用于非阻塞式的响应式编程。

特点

  • 异步非阻塞:适合响应式环境,支持 Reactor 或 Vert.x 等异步框架。
  • 与 Reactive 数据库交互:支持非阻塞式的数据库操作。
  • 流式数据处理:可以处理数据流并逐步消费。

适用场景

  • 需要响应式架构的应用程序。
  • 使用异步数据库(如 PostgreSQL Reactive Driver)的场景。
  • 在高并发环境下需要更高性能的异步操作。

为什么可以替代?

尽管它们在特性上各有不同,但它们都可以被元模型生成器用作会话类型,因为:

  1. 核心功能相似

    • 它们都提供了与数据库交互的基本功能,如执行查询、插入、更新和删除操作。
    • 元模型生成器的查询方法需要一个接口来与数据库交互,无论是 EntityManager 还是 Session,本质上都可以满足这个需求。
  2. 适配不同场景

    • EntityManager 是 JPA 标准,适合通用的 JPA 应用。
    • Session 提供更直接的 Hibernate 特性,适合需要自定义行为的应用。
    • StatelessSession 适合高性能场景,如批量操作。
    • Mutiny.Session 适合响应式编程和异步操作。
  3. 灵活性

    • 元模型生成器本身设计灵活,允许开发者根据具体需求选择会话类型,只需保证生成的方法能够匹配选定的会话接口即可。

对比总结

特性EntityManagerSessionStatelessSessionMutiny.Session
是否为 JPA 标准
一级缓存
自动脏检查
高性能批量操作较弱一般很强强(异步)
支持异步
适用场景通用 JPA 应用Hibernate 原生应用高性能批量处理响应式编程和异步操作

选择哪种会话类型,取决于项目的需求和具体的使用场景。

编译时验证的价值

元模型生成器在编译时会验证:

  1. 抽象方法声明的参数是否与 HQL 查询的参数匹配。
    • 对于命名参数 :alice,必须存在一个名称为 alice 且类型完全匹配的方法参数。
    • 对于位置参数 ?2,方法的第二个参数必须完全匹配。
  2. 查询是否语法合法且语义正确,即查询中引用的实体、属性和函数必须存在且类型兼容。

使用 @HQL 注解的查询方法不需要额外添加 @CheckHQL 注解,Hibernate 会自动验证命名查询的合法性。

 @HQL 注解有一个名为 @SQL 的“伙伴”注解,它允许我们直接编写原生 SQL 查询,而不是使用 HQL。在这种情况下,元模型生成器对查询的合法性和类型校验能力会大大降低。

您可能会疑惑,是否应该在这里使用静态方法。这个设计还有待进一步探索。

6.3 生成实例方法的查询方法

我们之前看到的静态方法生成有一个缺点:无法在不影响客户端的情况下,透明地将生成的静态方法替换为改进后的手写实现。如果查询只在一个地方被调用(这种情况非常常见),这并不是大问题,因此静态方法在这种场景下是完全可以接受的。

然而,如果查询方法在多个地方被调用,则更好的选择是将其提升为某个类或接口的实例方法。这种转换非常简单。

我们只需在 Queries 接口中添加一个用于获取会话对象的抽象 getter 方法(同时从方法参数列表中移除会话对象)。此方法可以任意命名,例如:

interface Queries {
    EntityManager entityManager();

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

在这里,我们使用了 EntityManager 作为会话类型,但也可以选择其他会话类型。

此时,元模型生成器将生成以下内容:

@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 = new Queries_(entityManager);
List<Book> books = queries.findByTitleAndType(titlePattern, Type.BOOK);

如果我们希望将生成的查询方法替换为手写实现,只需将 Queries 接口中的抽象方法替换为默认方法,例如:

interface Queries {
    EntityManager entityManager();

    // 手写实现替换生成的实现
    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(FlushModeType.COMMIT)
            .setMaxResults(100)
            .getResultList();
    }
}

6.4 生成查找方法 (Finder Methods)

在某些简单情况下,可以根据方法签名推断出查询语句,从而不需要手动编写查询。这正是查找方法(Finder Methods)的目的。查找方法是使用 @Find 注解的方法,例如:

@Find
Book getBook(String isbn);

查找方法可以有多个参数:

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

查找方法的名称是任意的,并且没有语义要求。然而:

  • 返回类型 决定了要查询的实体类。
  • 方法的参数必须在 名称类型 上与实体类的字段完全匹配。

以第一个例子为例,Book 类中有一个持久化字段 String isbn,因此该查找方法是合法的。如果 Book 类中不存在名为 isbn 的字段,或者字段类型不同,那么方法声明会在编译时被拒绝,并提供有意义的错误提示。同样,第二个例子也是合法的,因为 Book 类包含 String titleType type 字段。

您可能会注意到,我们解决这个问题的方法与其他方式非常不同。在传统的 DAO 风格的仓储框架中,通常要求将查找方法的语义编码到方法名称中。这种思路来源于 Ruby,但我们认为它并不适合 Java。在 Java 中,这种方式显得非常不自然,而且从几乎所有角度来看,除了字符数量更少之外,这种方式都不如直接使用字符串字面量编写查询。字符串字面量至少能够包含空格和标点符号。而且,能够在不改变语义的情况下重命名一个查找方法,这显然是非常实用的 🙄。

生成的代码依赖于方法参数匹配的字段类型:

  • @Id 字段
    使用 EntityManager.find()

  • 所有 @NaturalId 字段
    使用 Session.byNaturalId()

  • 其他持久化字段,或字段类型的混合
    使用条件查询(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);

查找方法可以指定预取配置,例如:

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

 这种方式允许我们通过注解在 Book 类上声明哪些关联关系需要被预取(pre-fetch)。

6.5 分页和排序 (Paging and Ordering)

查询方法可以接受额外的“特殊”参数,这些参数不映射为查询的输入参数:

参数类型用途示例
Page指定查询结果的分页Page.first(20)
Order<? super E>指定排序字段,E 是实体类型Order.asc(Book_.title)
List<Order<? super E>>指定多个排序字段,E 是实体类型List.of(Order.asc(Book_.title), Order.asc(Book_.isbn))
Order<Object[]>指定排序列,适用于返回投影结果的查询Order.asc(1)
List<Object[]>指定多个排序列,适用于返回投影结果的查询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, 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));

6.6 查询和查找方法的返回类型

查询方法的返回类型可以灵活定义:

返回单个实体:

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

对于包含投影列表(Projection List)的查询方法,其返回值可以是 Object[]List<Object[]> 类型。例如:

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

如果查询的选择列表(Select List)中只有一个字段,那么方法的返回类型应直接使用该字段的类型。例如:

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

一个返回选择列表(Selection List)的查询方法,可以将结果重新封装为记录类型(Record),如我们在“表示投影列表”部分中看到的那样,例如:

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))
        .setPage(Page.page(RESULTS_PER_PAGE, page))
        .setFlushMode(FlushModeType.COMMIT)
        .setReadOnly(true)
        .setCacheStoreMode(CacheStoreMode.BYPASS)
        .setComment("Hello world!")
        .getResultList();

插入、更新和删除查询

必须返回 intvoid

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

查找方法的限制

另一方面,查找方法目前存在更多的限制。例如:

  • 查找方法的返回值必须是实体类型(如 Book),或实体类型的列表(如 List<Book>)。

正如您可能预料的,对于 Reactive 会话(如使用 Hibernate Reactive),所有的查询方法和查找方法的返回值必须是 Uni 类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

风间琉璃c

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

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

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

打赏作者

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

抵扣说明:

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

余额充值