Ohhnews

分类导航

$ cd ..
Baeldung原文

Hibernate中@Find注解详解:按方法命名约定生成仓库实现

#hibernate#@find注解#jakarta data#数据持久化#仓库模式

[LOADING...]

1. 引言

本文将介绍如何在 Hibernate 中使用 @Find 注解。我们将了解它是什么、有什么用途以及如何使用它。

2. 配置 Hibernate

在使用 @Find 注解之前,需要先配置 Hibernate 和数据库。

2.1. 依赖

要在 Hibernate 中使用 @Find,需要使用 6.3 或更高版本。撰写本文时最新版本是 7.4.6.Final:

$ xml
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>7.4.6.Final</version>
</dependency>

还需要 Jakarta Data Core API。撰写本文时最新版本是 1.0.2:

$ xml
<dependency>
    <groupId>jakarta.data</groupId>
    <artifactId>jakarta.data-api</artifactId>
    <version>1.0.2</version>
</dependency>

这样就有了编写仓储代码所需的一切。

2.2. Hibernate 处理器

除了编写仓储所需的依赖外,还需要配置 Hibernate Processor。它会在编译时为我们仓储接口生成具体类。

使用 Hibernate 7 时,通过向 maven-compiler-plugin 添加注解处理器来配置:

$ xml
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.hibernate.orm</groupId>
                        <artifactId>hibernate-processor</artifactId>
                        <version>7.4.6.Final</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

它需要与之前指定的 hibernate-core 依赖版本一致。

插件添加后,Maven 构建会自动为我们生成额外类,包括实体的 Hibernate Metamodel 类,以及实现仓储接口的具体类,稍后会看到。

2.3. 数据库

本文需要设置数据库。表结构如下:

$ query
CREATE TABLE authors (
  author_id   BIGINT PRIMARY KEY,
  name        TEXT NOT NULL
);
CREATE TABLE books (
  book_id     BIGINT PRIMARY KEY,
  title       TEXT NOT NULL,
  author_id   BIGINT NOT NULL,
  FOREIGN KEY (author_id) REFERENCES authors(author_id)
);

这会有两张表 —— books 和 authors —— 它们之间有外键。

然后需要在数据库中添加一些数据:

$ query
INSERT INTO authors (author_id, name) VALUES
  (1, 'George Orwell'),
  (2, 'Haruki Murakami'),
  (3, 'Agatha Christie'),
  (4, 'Ursula K. Le Guin');
INSERT INTO books (book_id, title, author_id) VALUES
  (101, '1984', 1),
  (102, 'Animal Farm', 1),
  (103, 'Norwegian Wood', 2),
  (104, 'Kafka on the Shore', 2),
  (105, 'Murder on the Orient Express', 3),
  (106, 'And Then There Were None', 3),
  (107, 'The Left Hand of Darkness', 4);

2.4. JPA 实体

最后,由于使用 Hibernate,还需要 Hibernate 实体来表示数据。

首先是 Author 实体:

$ java
@Entity
@Table(name = "authors")
public class Author {
    @Id
    @Column(name = "author_id")
    private Long authorId;
    private String name;
    public Long getAuthorId() {
        return authorId;
    }
    public String getName() {
        return name;
    }
}

然后是 Book 实体:

$ java
@Entity
@Table(name = "books")
public class Book {
    @Id
    @Column(name = "book_id")
    private Long bookId;
    private String title;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id", nullable = false)
    private Author author;
    public Long getBookId() {
        return bookId;
    }
    public String getTitle() {
        return title;
    }
    public Author getAuthor() {
        return author;
    }
}

它引用了 Author 实体,因此可以在查询中沿着这些关联进行查询。

3. 仓储接口

一切设置完成后,就可以开始编写仓储了。这些仓储以 Java 接口形式编写,并标注 @jakarta.data.repository.Repository 注解:

$ java
@Repository
public interface BookRepository {
}

仅此就足以让 Hibernate Processor 发现并生成具体实现 —— 在本例中,生成的类名为 BookRepository_。

$ java
// Generated code
public class BookRepository_ implements BookRepository {
    protected StatelessSession session;
    public BookRepository_(StatelessSession session) {
        this.session = session;
    }
    public StatelessSession session() {
        return session;
    }
}

这会生成一个使用 StatelessSession 实例构造的实现。之后在需要查询数据时,可以按需创建它的实例。

3.1. 注入 EntityManager

我们通常不希望按需创建仓储实例,而是希望在应用程序启动时创建一次,然后到处传递。例如,可能希望在 Spring 上下文中创建它们。

幸运的是,Hibernate 也支持这样做。只需向接口添加一个返回 EntityManager 的特殊方法:

$ java
@Repository
public interface AuthorRepository {
    EntityManager entityManager();
}

如果这样做,Hibernate 会构造仓储的另一种形式,改用 EntityManager 来构造:

$ java
// Generated code
public class AuthorRepository_ implements AuthorRepository {
    protected EntityManager entityManager;
    public AuthorRepository_(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
    @Override
    public EntityManager entityManager() {
        return entityManager;
    }
}

之后就可以安全地只构造一次,并在需要时反复使用。

4. 按字段查找

有了仓储后,需要能够用它做事情。可以添加使用 @Find 注解的方法来查找实体,并对参数名和返回类型有特殊约定。

$ java
@Repository
public interface BookRepository {
    @Find
    List<Book> getAllBooks();
}

这里有一个名为 getAllBooks 的方法。方法名不重要。但它返回 List<Book> 这一点意味着生成的代码会理解我们在处理 Book 实体,并返回所有匹配查询的实体。

这等价于运行以下查询:

$ query
SELECT b1_0.book_id, b1_0.author_id, b1_0.title 
FROM books b1_0

可以通过实际过滤结果更进一步:

$ java
@Find
Book getBookWithTitle(String title);

同样,方法名可以是任何名称。但参数名必须与 Book 实体中的字段完全匹配。因此,Hibernate 会生成按该字段过滤的代码:

$ query
SELECT b1_0.book_id, b1_0.author_id, b1_0.title 
FROM books b1_0 
WHERE b1_0.title = ?

这次返回单个 Book 实体。因此 Hibernate 知道要返回单个匹配实体。如果没有匹配项,则会抛出 EmptyResultException。或者,如果有多个匹配记录,则会抛出 NonUniqueResultException。

5. 可选结果

有时我们要查找可能不存在的单条记录。可以处理抛出的 EmptyResultException,但这并不理想。

Hibernate 支持几种自动处理方式。最明显的是将方法返回类型改为 Optional<Book>:

$ java
@Find
Optional<Book> getOptionalBookWithTitle(String title);

在这种情况下,找不到记录时会返回 Optional.empty()。

或者,如果不想处理这些,可以用 jakarta.annotation.Nullable 标注方法:

$ java
@Find
@Nullable
Book getNullableBookWithTitle(String title);

这会告诉生成的代码返回 null,而不是抛出异常。

这两种情况下,数据库查询完全相同,唯一区别是生成的代码在执行查询后如何处理结果。如果查询返回多条结果,两种情况仍然都会抛出 NonUniqueResultException。

6. 按嵌套字段查找

到目前为止,我们已经看到如何根据记录本身的数据查找记录。但通常也会想根据关联数据进行搜索。Hibernate 可以根据方法参数命名来实现。如果使用 $ 符号,它会被解释为通过关联实体链接字段:

$ java
@Find
List<Book> getAllBooksByAuthorName(String author$name);

这里查询会从 Book 通过 Book.author 字段连接,然后根据 Author.name 查询:

$ query
SELECT b1_0.book_id, b1_0.author_id, b1_0.title 
FROM books b1_0 
  JOIN authors a1_0 ON a1_0.author_id = b1_0.author_id 
WHERE a1_0.name = ?

这种技术可以按需使用多步。但是只能沿着使用 @ManyToOne 或 @OneToOne 的关系。使用 @OneToMany 或 @ManyToMany 的关系不行,因为生成的查询需要链接到多条记录。

7. 多个结果

到目前为止,我们看到如何返回单个结果或所有匹配结果。但有时需要更多控制。Hibernate 可以为我们管理所有这些。

7.1. 排序

默认情况下,返回记录列表的查询会按数据库返回的顺序排列。这可能会因多种因素而变化,因此不能依赖它来获得一致顺序。

如果为方法添加 @OrderBy 注解,Hibernate 会按指定字段对结果排序:

$ java
@Find
@OrderBy("title")
List<Book> getAllBooks();

这等价于执行以下查询:

$ query
SELECT b1_0.book_id, b1_0.author_id, b1_0.title 
FROM books b1_0 
ORDER BY b1_0.title

添加了 ORDER BY 子句以实现一致排序。

如有必要,可以重复该注解以按多个字段排序:

$ java
@Find
@OrderBy("title")
@OrderBy("author$name")
List<Book> getAllBooks();

注意,也可以使用与前面完全相同的语法,按关联实体上的字段排序。这样会生成如下查询:

$ query
SELECT b1_0.book_id, b1_0.author_id, b1_0.title 
FROM books b1_0 
  JOIN authors a1_0 ON a1_0.author_id = b1_0.author_id 
ORDER BY b1_0.title desc, a1_0.name

还可以使用 descending 参数指定排序方向。它默认为 false,但可以改为指示降序:

$ java
@Find
@OrderBy(value = "title", descending = true)
List<Book> getAllBooks();

不出所料,可以按需混合使用这些,从而按多个字段、不同方向排序。

还可以接受 Order<Book> 类型的参数来允许动态排序。

$ java
@Find
List<Book> getAllBooks(Order<Book> sort);

它的泛型是正在处理的实体。如果使用错误的泛型类型,Hibernate 会在编译时抛出错误。现在可以这样调用并在运行时定义排序:

$ java
List<Book> books = repository.getAllBooks(Order.by(
  Sort.asc("title"),
  Sort.desc("author.name")
));

注意,这里需要使用点号语法指定嵌套字段,而不是 $ 符号,但其他方面工作方式相同。

7.2. 分页

除了排序结果外,还可以请求结果的特定页。可以通过向方法添加 PageRequest 类型的参数来实现:

$ java
@Find
@OrderBy("title")
List<Book> getBooksPage(PageRequest pageRequest);

从技术上讲,不必强制这些查询排序,但通常这样做是个好主意,以便结果在各页之间保持一致。

然后可以调用它并指定所需页码和每页大小:

$ java
List<Book> books = repository.getBooksPage(PageRequest.ofPage(1, 3, true));

注意,页码从 1 开始,因此这会返回 3 条记录的第一页。

如果想了解更多页的详细信息,需要将返回类型从 List<Book> 改为 Page<Book>:

$ java
@Find
@OrderBy("title")
Page<Book> getBooksPage(PageRequest pageRequest);

现在不仅可以访问页面内容,还可以访问页面详情,如元素总数以及是否有下一页和上一页:

$ java
long total = books.totalElements();
if (books.hasNext()) {
    // Do something
}
if (books.hasPrevious()) {
    // Do something
}

注意,PageRequest.ofPage() 调用的第三个参数告诉 Hibernate 我们是否需要所有页中元素的总数。如果传入 true,Hibernate 会执行额外的查询来统计匹配元素。如果传入 false,Hibernate 不会执行该额外查询,元素总数也不可用。

8. 总结

本文简要介绍了在 Hibernate 中使用 @Find 注解。我们了解了它是什么、如何工作以及如何使用。下次需要生成这样的仓储时,何不尝试一下?

一如既往,本文所有代码都可以在 GitHub 上找到。

文章 The @Find Annotation in Hibernate 首次出现在 Baeldung。