1. Overview
1.概述
Spring JPA provides a very flexible and convenient API for interaction with databases. However, sometimes, we need to customize it or add more functionality to the returned collections.
Spring JPA 为与数据库交互提供了非常灵活方便的 API。然而,有时我们需要对其进行自定义,或为返回的集合添加更多功能。
Using Map as a return type from JPA repository methods might help to create more straightforward interactions between services and databases. Unfortunately, Spring doesn’t allow this conversion to happen automatically. In this tutorial, we’ll check how to overcome this and learn some interesting techniques to make our repositories more functional.
使用 Map 作为 JPA 资源库方法的返回类型可能有助于在服务和数据库之间创建更直接的交互。遗憾的是,Spring 并不允许自动进行这种转换。在本教程中,我们将了解如何克服这一问题,并学习一些有趣的技术,使我们的存储库更具功能性。
2. Manual Implementation
2.手册实施
The most apparent approach to the problem when a framework doesn’t provide something, is to implement it ourselves. In this case, JPA allows us to implement the repositories from scratch, skip the entire generation process, or use default methods to get the best of both worlds.
当框架不提供某些功能时,最明显的解决方法就是自己实现。在这种情况下,JPA 允许我们从头开始实现存储库,跳过整个生成过程,或者使用默认方法来获得两全其美的效果。
2.1. Using List
2.1.使用 List
We can implement a method to map the resulting list into the map. Stream API helps greatly with this task, allowing almost one-liner implementation:
我们可以实现一种方法,将生成的列表映射到 map 中。流 API 可以极大地帮助我们完成这项任务,几乎只需单行程序即可实现:
default Map<Long, User> findAllAsMapUsingCollection() {
return findAll().stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
}
2.2. Using Stream
2.2.使用 Stream
We can do a similar thing but use Stream directly. To do so, we can identify a custom method that will return a stream of users. Luckily, Spring JPA supports such return types, and we can benefit from autogeneration:
我们可以直接使用 Stream 做类似的事情。为此,我们可以确定一个将返回用户流的自定义方法。幸运的是,Spring JPA 支持此类返回类型,我们可以从自动生成中受益:
@Query("select u from User u")
Stream<User> findAllAsStream();
After that, we can implement a custom method that would map the results into the data structure we need:
之后,我们可以实现一个自定义方法,将结果映射到我们需要的数据结构中:
@Transactional
default Map<Long, User> findAllAsMapUsingStream() {
return findAllAsStream()
.collect(Collectors.toMap(User::getId, Function.identity()));
}
The repository methods that return Stream should be called inside a transaction. In this case, we directly added a @Transactional annotation to the default method.
返回 Stream 的版本库方法应在事务中调用。在这种情况下,我们直接在默认方法中添加了 @Transactional 注解。
2.3. Using Streamable
2.3.使用 Streamable
This is a similar approach to the one discussed previously. The only change is that we’ll be using Streamable. We need to create a custom method to return it first:
这与之前讨论的方法类似。唯一的变化是我们将使用 Streamable。我们需要先创建一个自定义方法来返回它:
@Query("select u from User u")
Streamable<User> findAllAsStreamable();
Then, we can map the result appropriately:
然后,我们就可以对结果进行适当的映射:
default Map<Long, User> findAllAsMapUsingStreamable() {
return findAllAsStreamable().stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
}
3. Custom Streamable Wrapper
3.自定义可流式包装器
Previous examples showed us quite simple solutions to the problem. However, suppose we have several different operations or data structures to which we want to map our results. In that case, we can end up with unwieldy mappers scattered around our code or multiple repository methods that do similar things.
前面的例子向我们展示了非常简单的解决方案。但是,假设我们有多个不同的操作或数据结构,我们希望将结果映射到这些操作或数据结构。在这种情况下,我们的代码中可能会散布着笨重的映射器,或者有多个存储库方法在做类似的事情。
A better approach might be to create a dedicated class representing a collection of entities and place all the methods connected to the operations on the collection inside. To do so, we’ll be using Streamable.
更好的方法可能是创建一个代表实体集合的专用类,并将与集合上的操作相关的所有方法都放在该类中。为此,我们将使用 Streamable。
As was shown previously, Spring JPA understands Streamable and can map the result to it. Interestingly, we can extend Streamable and provide it with convenient methods. Let’s create a Users class that would represent a collection of User objects:
如前所述,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
}
To make it work with JPA, we should follow a simple convention. First, we should implement Streamable, and secondly, provide the way Spring will be able to initialize it. The initialization part can be addressed either by a public constructor that takes Streamable or static factories with names of(Streamable<T>) or valueOf(Streamable<T>).
为了让它与 JPA 配合使用,我们应该遵循一个简单的惯例。首先,我们应该实现 Streamable,其次,提供 Spring 能够初始化它的方法。初始化部分可以通过使用 Streamable 的公共构造函数或名称为 of(Streamable<T>) 或 valueOf(Streamable<T>) 的静态工厂来解决。
After that, we can use Users as a return type of JPA repository methods:
之后,我们可以使用 Users 作为 JPA 储藏库方法的返回类型:
@Query("select u from User u")
Users findAllUsers();
Now, we can place the method we kept in the repository directly in the Users class:
现在,我们可以直接在 Users 类中放置我们在版本库中保留的方法:
public Map<Long, User> getUserIdToUserMap() {
return stream().collect(Collectors.toMap(User::getId, Function.identity()));
}
The best part is that we can use all the methods connected to the processing or mapping of the User entities. Let’s say we want to filter out users by some criteria:
最棒的是,我们可以使用与 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);
}
Also, we can group them in some way:
此外,我们还可以通过某种方式对它们进行分组:
@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);
}
This way, we can hide the implementation of such methods, remove clutter from our services, and unload the repositories.
这样,我们就可以隐藏这些方法的实现,去除服务中的杂乱无章,并卸载资源库。
4. Conclusion
4.结论
Spring JPA allows customization, but sometimes it’s pretty straightforward to achieve this. Building an application around the types restricted by a framework might affect the quality of the code and even the design of an application.
Spring JPA 允许自定义,但有时实现自定义非常简单。围绕框架限制的类型构建应用程序可能会影响代码的质量,甚至影响应用程序的设计。
Using custom collections as return types might make the design more straightforward and less cluttered with mapping and filtering logic. Using dedicated wrappers for the collections of entities can improve the code even further.
使用自定义集合作为返回类型可能会使设计更加简单明了,减少映射和过滤逻辑的干扰。为实体集合使用专用封装器可以进一步改进代码。
As usual, all the code used in this tutorial is available over on GitHub.
与往常一样,本教程中使用的所有代码均可在 GitHub 上获取。