Hibernate中@Find注解详解:按方法命名约定生成仓库实现
[LOADING...]
1. 引言
本文将介绍如何在 Hibernate 中使用 @Find 注解。我们将了解它是什么、有什么用途以及如何使用它。
2. 配置 Hibernate
在使用 @Find 注解之前,需要先配置 Hibernate 和数据库。
2.1. 依赖
要在 Hibernate 中使用 @Find,需要使用 6.3 或更高版本。撰写本文时最新版本是 7.4.6.Final:
还需要 Jakarta Data Core API。撰写本文时最新版本是 1.0.2:
这样就有了编写仓储代码所需的一切。
2.2. Hibernate 处理器
除了编写仓储所需的依赖外,还需要配置 Hibernate Processor。它会在编译时为我们仓储接口生成具体类。
使用 Hibernate 7 时,通过向 maven-compiler-plugin 添加注解处理器来配置:
它需要与之前指定的 hibernate-core 依赖版本一致。
插件添加后,Maven 构建会自动为我们生成额外类,包括实体的 Hibernate Metamodel 类,以及实现仓储接口的具体类,稍后会看到。
2.3. 数据库
本文需要设置数据库。表结构如下:
这会有两张表 —— books 和 authors —— 它们之间有外键。
然后需要在数据库中添加一些数据:
2.4. JPA 实体
最后,由于使用 Hibernate,还需要 Hibernate 实体来表示数据。
首先是 Author 实体:
然后是 Book 实体:
它引用了 Author 实体,因此可以在查询中沿着这些关联进行查询。
3. 仓储接口
一切设置完成后,就可以开始编写仓储了。这些仓储以 Java 接口形式编写,并标注 @jakarta.data.repository.Repository 注解:
仅此就足以让 Hibernate Processor 发现并生成具体实现 —— 在本例中,生成的类名为 BookRepository_。
这会生成一个使用 StatelessSession 实例构造的实现。之后在需要查询数据时,可以按需创建它的实例。
3.1. 注入 EntityManager
我们通常不希望按需创建仓储实例,而是希望在应用程序启动时创建一次,然后到处传递。例如,可能希望在 Spring 上下文中创建它们。
幸运的是,Hibernate 也支持这样做。只需向接口添加一个返回 EntityManager 的特殊方法:
如果这样做,Hibernate 会构造仓储的另一种形式,改用 EntityManager 来构造:
之后就可以安全地只构造一次,并在需要时反复使用。
4. 按字段查找
有了仓储后,需要能够用它做事情。可以添加使用 @Find 注解的方法来查找实体,并对参数名和返回类型有特殊约定。
这里有一个名为 getAllBooks 的方法。方法名不重要。但它返回 List<Book> 这一点意味着生成的代码会理解我们在处理 Book 实体,并返回所有匹配查询的实体。
这等价于运行以下查询:
可以通过实际过滤结果更进一步:
同样,方法名可以是任何名称。但参数名必须与 Book 实体中的字段完全匹配。因此,Hibernate 会生成按该字段过滤的代码:
这次返回单个 Book 实体。因此 Hibernate 知道要返回单个匹配实体。如果没有匹配项,则会抛出 EmptyResultException。或者,如果有多个匹配记录,则会抛出 NonUniqueResultException。
5. 可选结果
有时我们要查找可能不存在的单条记录。可以处理抛出的 EmptyResultException,但这并不理想。
Hibernate 支持几种自动处理方式。最明显的是将方法返回类型改为 Optional<Book>:
在这种情况下,找不到记录时会返回 Optional.empty()。
或者,如果不想处理这些,可以用 jakarta.annotation.Nullable 标注方法:
这会告诉生成的代码返回 null,而不是抛出异常。
这两种情况下,数据库查询完全相同,唯一区别是生成的代码在执行查询后如何处理结果。如果查询返回多条结果,两种情况仍然都会抛出 NonUniqueResultException。
6. 按嵌套字段查找
到目前为止,我们已经看到如何根据记录本身的数据查找记录。但通常也会想根据关联数据进行搜索。Hibernate 可以根据方法参数命名来实现。如果使用 $ 符号,它会被解释为通过关联实体链接字段:
这里查询会从 Book 通过 Book.author 字段连接,然后根据 Author.name 查询:
这种技术可以按需使用多步。但是只能沿着使用 @ManyToOne 或 @OneToOne 的关系。使用 @OneToMany 或 @ManyToMany 的关系不行,因为生成的查询需要链接到多条记录。
7. 多个结果
到目前为止,我们看到如何返回单个结果或所有匹配结果。但有时需要更多控制。Hibernate 可以为我们管理所有这些。
7.1. 排序
默认情况下,返回记录列表的查询会按数据库返回的顺序排列。这可能会因多种因素而变化,因此不能依赖它来获得一致顺序。
如果为方法添加 @OrderBy 注解,Hibernate 会按指定字段对结果排序:
这等价于执行以下查询:
添加了 ORDER BY 子句以实现一致排序。
如有必要,可以重复该注解以按多个字段排序:
注意,也可以使用与前面完全相同的语法,按关联实体上的字段排序。这样会生成如下查询:
还可以使用 descending 参数指定排序方向。它默认为 false,但可以改为指示降序:
不出所料,可以按需混合使用这些,从而按多个字段、不同方向排序。
还可以接受 Order<Book> 类型的参数来允许动态排序。
它的泛型是正在处理的实体。如果使用错误的泛型类型,Hibernate 会在编译时抛出错误。现在可以这样调用并在运行时定义排序:
注意,这里需要使用点号语法指定嵌套字段,而不是 $ 符号,但其他方面工作方式相同。
7.2. 分页
除了排序结果外,还可以请求结果的特定页。可以通过向方法添加 PageRequest 类型的参数来实现:
从技术上讲,不必强制这些查询排序,但通常这样做是个好主意,以便结果在各页之间保持一致。
然后可以调用它并指定所需页码和每页大小:
注意,页码从 1 开始,因此这会返回 3 条记录的第一页。
如果想了解更多页的详细信息,需要将返回类型从 List<Book> 改为 Page<Book>:
现在不仅可以访问页面内容,还可以访问页面详情,如元素总数以及是否有下一页和上一页:
注意,PageRequest.ofPage() 调用的第三个参数告诉 Hibernate 我们是否需要所有页中元素的总数。如果传入 true,Hibernate 会执行额外的查询来统计匹配元素。如果传入 false,Hibernate 不会执行该额外查询,元素总数也不可用。
8. 总结
本文简要介绍了在 Hibernate 中使用 @Find 注解。我们了解了它是什么、如何工作以及如何使用。下次需要生成这样的仓储时,何不尝试一下?
一如既往,本文所有代码都可以在 GitHub 上找到。
文章 The @Find Annotation in Hibernate 首次出现在 Baeldung。