Spring Data JPA 中返回映射而不是列表

使用Map 作为 JPA 存储库方法的返回类型可能有助于在服务和数据库之间创建更直接的交互。不幸的是,Spring 不允许这种转换自动发生。在本教程中,我们将检查如何克服这个问题并学习一些有趣的技术来使我们的存储库更加实用。

我们可以实现一种方法将结果列表映射到映射中。Stream API对这项任务有很大帮助,几乎允许单行实现:

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

或直接使用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注解。

第三种办法:将使用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()));
}

自定义流式包装器
然而,假设我们有几种不同的操作或数据结构,我们想要将结果映射到这些操作或数据结构。在这种情况下,我们最终可能会得到分散在代码中的笨拙的映射器或执行类似操作的多个存储库方法。

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

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

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 一起工作,我们应该遵循一个简单的约定。

  1. 首先,我们应该实现Streamable,
  2. 其次,提供 Spring 能够初始化它的方式。

初始化部分可以通过采用Streamable 的公共构造函数 或名称为 (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()));
}

最后,是我们可以使用与用户实体的处理或映射相关的所有方法。
假设我们想按某些标准过滤掉用户:
@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);
}

通过这种方式,我们可以隐藏此类方法的实现,消除服务中的混乱,并卸载存储库。

使用自定义集合作为返回类型可能会使设计更加简单,并且不会因映射和过滤逻辑而变得混乱。使用实体集合的专用包装器可以进一步改进代码。