1. 概述

Spring JPA 提供了一个非常灵活且方便的与数据库交互的API。然而,有时我们需要自定义它或为返回的集合添加更多功能。

使用Java的Map作为JPA仓库方法的返回类型,可以帮助服务和数据库之间的交互更加直接。不幸的是,Spring并未自动进行这种转换。 在本教程中,我们将探讨如何解决这个问题,并学习一些使我们的仓库更具功能的有趣技巧。

2. 手动实现

当框架不提供某种功能时,最明显的解决方案是自己实现。在这种情况下,JPA允许我们从头开始实现仓库,跳过整个生成过程,或者利用默认方法来兼顾两者的优势。

2.1. 使用List

我们可以实现一个方法,将结果列表映射到映射中。Stream API 对此任务大有帮助,几乎可以实现一行代码:

default Map<Long, User> findAllAsMapUsingCollection() {
    return findAll().stream()
      .collect(Collectors.toMap(User::getId, Function.identity()));
}

2.2. 使用Stream

我们可以做类似的事情,但直接使用Stream。为此,我们可以识别一个自定义方法,该方法将返回用户流。幸运的是,Spring JPA支持此类返回类型,我们可以从中受益于自动生成:

@Query("select u from User u")
Stream<User> findAllAsStream();

然后,我们可以实现一个自定义方法,将结果映射到我们需要的数据结构:

@Transactional
default Map<Long, User> findAllAsMapUsingStream() {
    return findAllAsStream()
      .collect(Collectors.toMap(User::getId, Function.identity()));
}

返回Stream的仓库方法应在事务中调用。在这种情况下,我们在默认方法上直接添加了@Transactional注解。

2.3. 使用Streamable

这是之前讨论过的类似方法。唯一的区别是我们将使用Streamable。首先,我们需要创建一个自定义方法来返回它:

@Query("select u from User u")
Streamable<User> findAllAsStreamable();

然后,我们可以适当地映射结果:

default Map<Long, User> findAllAsMapUsingStreamable() {
    return findAllAsStreamable().stream()
      .collect(Collectors.toMap(User::getId, Function.identity()));
}

3. 自定义Streamable包装器

先前的示例向我们展示了问题的简单解决方案。然而,如果我们想将结果映射到多个不同的操作或数据结构,可能会导致代码中分散的笨拙映射器或执行相似任务的多个仓库方法。

更好的做法可能是创建一个专门表示实体集合的类,并将所有与集合相关的方法放在其中。为此,我们将使用Streamable

如前所述,Spring JPA理解Streamable并能将其结果映射到它。有趣的是,我们可以扩展Streamable并为其提供便利的方法。让我们创建一个表示User对象集合的Users类:

public class Users implements Streamable<User> {

    private final Streamable<User> userStreamable;

    public Users(Streamable<User> userStreamable) {
        this.userStreamable = userStreamable;
    }

    @Override
    public Iterator<User> iterator() {
        return userStreamable.iterator();
    }

    // custom methods
}

为了使其与JPA配合工作,我们应该遵循一个简单的约定。首先,我们需要实现Streamable,其次,提供Spring能够初始化它的方法。初始化部分可以通过接受Streamable的公共构造函数或带有名称of(Streamable<T>)valueOf(Streamable<T>)的静态工厂来处理。

之后,我们可以将Users用作JPA仓库方法的返回类型:

@Query("select u from User u")
Users findAllUsers();

现在,我们可以将之前在仓库中保留的方法直接放在Users类中:

public Map<Long, User> getUserIdToUserMap() {
    return stream().collect(Collectors.toMap(User::getId, Function.identity()));
}

最棒的是,我们可以使用所有与处理或映射User实体相关的方法。假设我们想根据某些标准过滤用户:

@Test
void fetchUsersInMapUsingStreamableWrapperWithFilterThenAllOfThemPresent() {
    Users users = repository.findAllUsers();
    int maxNameLength = 4;
    List<User> actual = users.getAllUsersWithShortNames(maxNameLength);
    User[] expected = {
        new User(9L, "Moe", "Oddy"),
        new User(25L, "Lane", "Endricci"),
        new User(26L, "Doro", "Kinforth"),
        new User(34L, "Otho", "Rowan"),
        new User(39L, "Mel", "Moffet")
    };
    assertThat(actual).containsExactly(expected);
}

同样,我们也可以以某种方式对它们进行分组:

@Test
void fetchUsersInMapUsingStreamableWrapperAndGroupingThenAllOfThemPresent() {
    Users users = repository.findAllUsers();
    Map<Character, List<User>> alphabeticalGrouping = users.groupUsersAlphabetically();
    List<User> actual = alphabeticalGrouping.get('A');
    User[] expected = {
        new User(2L, "Auroora", "Oats"),
        new User(4L, "Alika", "Capin"),
        new User(20L, "Artus", "Rickards"),
        new User(27L, "Antonina", "Vivian")};
    assertThat(actual).containsExactly(expected);
}

这样,我们可以隐藏这些方法的实现,减少服务中的混乱,并减轻仓库的负担。

4. 总结

Spring JPA允许自定义,但在某些情况下,实现这一点相当直接。围绕框架限制的类型构建应用程序可能会影响代码质量,甚至影响应用程序设计。

使用自定义集合作为返回类型可以使设计更直观,减少映射和过滤逻辑的混乱。使用专门的实体集合包装器可以进一步改进代码。

如往常一样,本教程中使用的所有代码都可以在GitHub上找到。