概述

本教程将探讨在Spring Data JPA的衍生查询API中,findByfindAllBy 方法命名约定的区别。

1. 引言

Spring Data JPA支持基于方法名称的衍生查询。这意味着如果我们使用特定的关键字在方法名中,就不需要手动指定查询语句。

findBy 关键字一起工作,根据规则生成一个搜索集合结果的查询。请注意,这两个关键字返回的结果形式都是集合,这可能导致对findAllBy 的使用产生混淆。在Spring Data文档中,并没有定义All关键字(https://docs.spring.io/spring-data/jpa/reference/repositories/query-keywords-reference.html`_)。

接下来的章节我们将验证findByfindAllBy 在Spring Data JPA中并无差异,并提供一种替代方法来查找单个结果而不是集合。

1.1 示例应用

首先,我们定义一个示例Spring Data应用。然后创建Player实体类:

@Entity
public class Player {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private Integer score;

    //all-arg and no-arg constructors
    //overriden equals method
    //getters and setters
}

接下来,创建扩展自JpaRepository接口的PlayerRepository接口:

@Repository
public interface PlayerRepository extends JpaRepository<Player, Long> {
}

1.2 findBy 查询示例

如前所述,findBy 关键字使用规则返回一个集合的结果。这个规则位于By关键字之后。让我们在PlayerRepository类中创建一个方法,衍生出一个查询,找到分数高于给定输入的所有玩家:

List<Player> findByScoreGreaterThan(Integer target);

Spring Data JPA会解析方法名称语法并转换成SQL语句来生成查询。让我们看看每个关键字的作用:

  • find 被翻译成select语句。
  • By 被解析为where子句。
  • Score是表列名,应与Player类中定义的相同。
  • GreaterThan在查询中添加>操作符,将score字段与方法参数target进行比较。

1.3 findAllBy 查询示例

findBy类似,我们在PlayerRepository类中创建一个带有All关键字的方法:

List<Player> findAllByScoreGreaterThan(Integer target);

这个方法的工作方式类似于findByScoreGreaterThan()方法——唯一的区别是All关键字。这个关键字只是一个命名约定,并不会为衍生查询增加任何功能,我们将在下一节中看到这一点。

2. findBy vs. findAllBy

现在,让我们验证findByfindAllBy之间的差异仅限于命名约定,并证明它们在功能上是相同的。

2.1 功能性差异

为了分析两者之间是否存在功能上的差异,让我们编写一个集成测试:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = FindByVsFindAllByApplication.class)
public class FindByVsFindAllByIntegrationTest {
    @Autowired
    private PlayerRepository playerRepository;

    @Before
    public void setup() {
        Player player1 = new Player(600);
        Player player2 = new Player(500);
        Player player3 = new Player(300);
        playerRepository.saveAll(Arrays.asList(player1, player2, player3));
    }

    @Test
    public void givenSavedPlayer_whenUseFindByOrFindAllBy_thenReturnSameResult() {
        List<Player> findByPlayers = playerRepository.findByScoreGreaterThan(400);
        List<Player> findAllByPlayers = playerRepository.findAllByScoreGreaterThan(400);
        assertEquals(findByPlayers, findAllByPlayers);
    }
}

注意:为了使测试通过,Player实体必须重写equals()方法,比较idscore字段。

两个方法返回的结果与assertEquals()显示的一样,因此在功能上没有区别。

2.2 查询语法差异

为了完整起见,让我们比较由两种方法生成的查询的语法。为此,我们需要在application.properties文件中添加以下行:

spring.jpa.show-sql=true

如果重新运行集成测试,两个查询应该会出现在控制台。这是findByScoreGreaterThan()的衍生查询:

select
    player0_.id as id1_0_, player0_.score as score2_0_ 
from
    player player0_ 
where
    player0_.score>?

findAllByScoreGreaterThan()的衍生查询:

select
    player0_.id as id1_0_, player0_.score as score2_0_
from
    player player0_
where
    player0_.score>?

我们可以看到,生成的查询语法没有差异。因此,在代码风格上,findByfindAllBy关键字的唯一区别在于我们选择采用哪种方式。我们可以使用任意一个,期望得到相同的结果。

3. 返回单个结果

我们已经确认了findByfindAllBy之间的差异仅限于命名约定,两者都返回集合结果。如果我们改变接口,从这些可能返回多个结果的生成查询中获取单个结果,我们可能会遇到NonUniqueResultException

在这个部分,我们将查看find**F**irstfind**T**op关键字,以衍生出一个查询,返回单个结果。

**F**irstT**op关键字应插入到findBy关键字之间,以查找存储的第一个元素。它们也可以与I**sGreaterThan等条件关键字一起使用。让我们看一个例子,找到分数大于400的存储的第一个玩家。首先,我们在PlayerRepository类中创建查询方法:

Optional<Player> findFirstByScoreGreaterThan(Integer target);

Top关键字在功能上等于First。它们之间的主要区别在于命名约定。因此,我们可以通过名为findTopByScoreGreaterThan()的方法实现相同的结果。

然后,我们验证只获取了一个单一的结果:

@Test
public void givenSavedPlayer_whenUsefindFirst_thenReturnSingleResult() {
    Optional<Player> player = playerRepository.findFirstByScoreGreaterThan(400);
    assertTrue(player.isPresent());
    assertEquals(600, player.get().getScore());
}

findFirstBy查询使用limit SQL运算符返回第一个匹配我们条件的存储元素,即具有id=1和score=600的Player

最后,让我们看看由我们的方法生成的查询:

select
    player0_.id as id1_0_, player0_.score as score2_0_
from
    player player0_
where
    player0_.score>?
limit ?

查询几乎与findByfindAllBy相同,只是在末尾添加了limit运算符。

4. 结论

在这篇文章中,我们探索了Spring Data JPA中findByfindAllBy关键字的相似之处。我们还研究了如何使用findFirstBy关键字获取单个结果。如往常一样,源代码可在GitHub上获取。