1. 概述

Spring Data repositories 提供了一系列方法,简化了数据访问逻辑的实现。然而,选择合适的方法并不总是像我们预期的那样简单。

一个例子是带有前缀 findByfindOneBy 的方法。尽管它们的名字看似相同,但它们之间有些微妙的区别。

2. Spring Data 中的派生查询方法

Spring Data JPA 的一个常见赞誉就是其派生查询方法特性。这些方法允许我们根据方法名自动生成特定的查询。例如,如果我们想通过 foo 属性获取数据,只需写 findByFoo() 即可。

通常,我们可以使用多个前缀来构建派生查询方法,包括 findByfindOneBy。现在让我们一起实践看看它们的工作原理。

3. 实践案例

首先,考虑一下 Person 实体类:

@Entity
public class Person {

    @Id
    private int id;
    private String firstName;
    private String lastName;

    // standard getters and setters
}

我们将使用 H2 作为数据库。先用一个基本的 SQL 脚本填充数据库:

INSERT INTO person (id, first_name, last_name) VALUES(1, 'Azhrioun', 'Abderrahim');
INSERT INTO person (id, first_name, last_name) VALUES(2, 'Brian', 'Wheeler');
INSERT INTO person (id, first_name, last_name) VALUES(3, 'Stella', 'Anderson');
INSERT INTO person (id, first_name, last_name) VALUES(4, 'Stella', 'Wheeler');

最后,我们创建一个 JPA repository 来管理 Person 实体:

@Repository
public interface PersonRepository extends JpaRepository<Person, Integer> {
}

3.1. findBy 前缀

findBy 是用于创建表示搜索查询的派生查询方法的常用前缀之一。动词 "find" 表示 Spring Data 将生成一个 select 查询,而 "By" 关键字则作为 where 子句,用于过滤返回的结果

接下来,我们在 PersonRepository 中添加一个按首名获取人的派生查询方法:

Person findByFirstName(String firstName);

如我们所见,这个方法返回一个 Person 对象。现在,我们添加一个测试用例来验证 findByFirstName() 方法:

@Test
void givenFirstName_whenCallingFindByFirstName_ThenReturnOnePerson() {
    Person person = personRepository.findByFirstName("Azhrioun");

    assertNotNull(person);
    assertEquals("Abderrahim", person.getLastName());
}

现在我们了解了如何使用 findBy 创建一个返回单个对象的查询方法,接下来让我们看看是否可以用它获取一个对象列表。为此,我们需要在 PersonRepository 中添加另一个方法:

List<Person> findByLastName(String lastName);

方法名称表明,这个新方法将帮助我们找到具有相同姓氏的所有对象。

同样地,我们用另一个测试用例验证 findByLastName()

@Test
void givenLastName_whenCallingFindByLastName_ThenReturnList() {
    List<Person> person = personRepository.findByLastName("Wheeler");

    assertEquals(2, person.size());
}

不出所料,测试成功通过。

简而言之,我们可以使用 findBy 获取单个对象或对象集合。

区分的关键在于查询方法的返回类型。Spring Data 根据返回类型来决定是返回一个还是多个对象。

3.2. findOneBy 前缀

通常,findOneBy 只是 findBy 的一个具体变体。它明确表示寻找恰好一条记录的意图。 让我们来看看它是如何工作的:

Person findOneByFirstName(String firstName);

接下来,我们添加一个测试用例确认我们的方法正常工作:

@Test
void givenFirstName_whenCallingFindOneByFirstName_ThenReturnOnePerson() {
    Person person = personRepository.findOneByFirstName("Azhrioun");

    assertNotNull(person);
    assertEquals("Abderrahim", person.getLastName());
}

那么,如果我们使用 findOneBy 获取对象列表会怎样呢?让我们来看看!

首先,我们在 PersonRepository 中添加一个查找所有同姓氏的 Person 对象的方法:

List<Person> findOneByLastName(String lastName);

接着,我们用一个测试用例测试我们的方法:

@Test
void givenLastName_whenCallingFindOneByLastName_ThenReturnList() {
    List<Person> persons = personRepository.findOneByLastName("Wheeler");

    assertEquals(2, persons.size());
}

如上所示,findOneByLastName() 返回一个列表,没有抛出任何异常。

从技术角度看,findOneByfindBy 没有区别。然而,使用 findOneBy 前缀创建返回集合的方法在语义上没有意义。

简而言之,findOneBy 前缀仅提供了一个返回单个对象的需求的语义描述。

Spring Data 依赖于 这个正则表达式,它忽略了 "find" 和 "By" 之间的所有字符。所以,findByfindOneByfindXyzBy……它们都是类似的。

创建派生查询方法时,有几个关键点需要记住:

  • 派生查询方法的重要部分是 "find" 和 "By" 关键词。
  • 我们可以在 "find" 和 "By" 之间添加单词以表示语义含义。
  • Spring Data 根据方法的返回类型决定返回单个对象还是对象集合。

4. IncorrectResultSizeDataAccessException

这里需要提到的一个重要注意事项是,findByLastName()findOneByLastName() 方法如果返回结果的大小不符合预期,都会抛出 IncorrectResultSizeDataAccessException 异常。

例如,Person findByFirstName(String firstName) 如果存在多个具有给定名字的 Person 对象,就会抛出异常。

现在,我们用一个测试用例来确认这一点:

@Test
void givenFirstName_whenCallingFindByFirstName_ThenThrowException() {
    IncorrectResultSizeDataAccessException exception = assertThrows(IncorrectResultSizeDataAccessException.class, () -> personRepository.findByFirstName("Stella"));

    assertEquals("query did not return a unique result: 2", exception.getMessage());
}

异常的原因是,尽管我们声明方法返回一个对象,但执行的查询返回了多条记录。

同样,我们用一个测试用例验证 findOneByFirstName() 抛出了 IncorrectResultSizeDataAccessException

@Test
void givenFirstName_whenCallingFindOneByFirstName_ThenThrowException() {
    IncorrectResultSizeDataAccessException exception = assertThrows(IncorrectResultSizeDataAccessException.class, () -> personRepository.findOneByFirstName("Stella"));

    assertEquals("query did not return a unique result: 2", exception.getMessage());
}

5. 总结

在这篇文章中,我们详细探讨了 Spring Data JPA 中 findByfindOneBy 前缀的相似性和差异。我们解释了 Spring Data JPA 的派生查询方法,并强调了尽管 findByfindOneBy 在语义上有不同,但它们在底层是相同的。

最后,我们展示了如果选择错误的返回类型,两者都会抛出 IncorrectResultSizeDataAccessException

一如既往,本文的完整代码可以在 GitHub 查看。