Spring 数据存储库:列表与流

Spring Data repository: list vs stream

在 Spring 数据存储库中定义方法 liststream 时有什么建议?

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-streaming

示例:

interface UserRepository extends Repository<User, Long> {

  List<User> findAllByLastName(String lastName);

  Stream<User> streamAllByFirstName(String firstName);                    
         
  // Other methods defined.
}

请注意,我在这里不是在询问 PageSlice - 我很清楚,我找到了他们的描述在 documentation.


我的假设(我错了吗?):

  1. Stream 不会将所有记录加载到 Java Heap。相反,它将 k 条记录加载到堆中并一条一条地处理它们;然后它加载另一个 k 记录等等。

  2. List 会立即将所有记录加载到 Java 堆中。

  3. 如果我需要一些后台批处理作业(例如计算分析),我可以使用流操作,因为我不会一次将所有记录加载到堆中。

  4. 如果我需要 return 包含所有记录的 REST 响应,无论如何我都需要将它们加载到 RAM 中并将它们序列化到 JSON 中。在这种情况下,一次加载列表是有意义的。


我看到一些开发人员在 return 响应之前将流收集到列表中。

class UserController {

    public ResponseEntity<List<User>> getUsers() {
        return new ResponseEntity(
                repository.streamByFirstName()
                        // OK, for mapper - it is nice syntactic sugar. 
                        // Let's imagine there is not map for now...
                        // .map(someMapper)  
                       .collect(Collectors.toList()), 
                HttpStatus.OK);
    }
}

对于这种情况,我看不出 Stream 有任何优势,使用 list 将得到相同的最终结果。

那么使用 list 是否合理?

StreamCollection 都有对象集合,但是 Collection 及其实现的问题是 Collection 实现拥有内存中的所有元素,实际上 Stream 在 Java8 中被引入来解决这个问题(以及其他一些问题)。想象一下如果 Collection 包含无限数量的元素会发生什么情况,你可以拥有无​​限数量的元素 Collection 吗?当然你不能,因为无论你的内存有多大,你都会在某个时候摆脱内存异常。但是 Stream 没有这个问题你可以有无限数量的元素 Stream 因为它们不存储在内存中,它们将按需生成。

回到你的问题,想象一下如果你有很多记录在你的第一个查询 findAllByLastName 中有 lastname 会发生什么?当然你会得到 OutOfMemoryError 异常,但是 Stream 解决了这个问题,无论有多少记录满足你的条件,你都不会得到 OutOfMemoryError 异常。 Stream 不在内存中加载对象它按需加载对象,因此它在大型结果查询上表现更好。

所以你的问题的答案:

  1. 是的,它按需将元素加载到内存,因此减少了内存消耗量和对数据库的查询调用量。

  2. 是的,当您调用该方法时,列表加载所有满足条件的记录。

  3. 是的,如果您想遍历满足某些条件的记录并做一些处理工作,您应该使用 Stream one。

  4. 这是一个棘手的问题,不知何故不,当您使用 WebFlux 和其他类似的反应式编程方法时,我认为最好选择 Stream一.

重要说明:如果您说某些开发人员在 return 响应之前将流收集到列表中,他们可以使用 WebFlux 和 return Stream 本身。这是更好的方法。

tl;博士

Collection VS Stream的主要区别有以下两个方面:

  1. 第一个结果的时间 – 客户端代码什么时候看到第一个元素?
  2. 处理时的资源状态 - 处理流时底层基础设施资源处于什么状态?

使用集合

让我们通过一个例子来说明这一点。假设我们需要从存储库中读取 100k Customer 个实例。您(必须)处理结果的方式给出了上述两个方面的提示。

List<Customer> result = repository.findAllBy();

一旦所有 元素已从基础数据存储中完全读取,客户端代码将收到该列表,而不是在此之前的任何时刻。而且,基础数据库连接 can 已关闭。 IE。例如在 Spring Data JPA 应用程序中,您将看到底层 EntityManager 已关闭并且实体已分离,除非您主动将其保留在更广泛的范围内,例如通过使用 @Transactional 或使用 OpenEntityManagerInViewFilter 注释周围的方法。另外,您不需要主动关闭资源。

使用流

流必须像这样处理:

@Transactional
void someMethod() {

  try (Stream result = repository.streamAllBy()) {
    // … processing goes here
  }
}

使用 Stream,处理可以在第一个元素(例如数据库中的行)到达并映射后立即开始。 IE。您将能够已经使用元素,而结果集中的其他元素仍在处理中。这也意味着,底层资源需要主动保持开放,因为它们通常绑定到存储库方法调用。请注意 Stream 还必须主动关闭(try-with-resources),因为它绑定了底层资源,我们必须以某种方式向它发出关闭它们的信号。

使用 JPA,如果没有 @TransactionalStream 将无法正确处理,因为基础 EntityManager 在方法 return 上关闭。您会看到一些元素已处理,但在处理过程中出现异常。

下游使用

所以虽然理论上您可以使用 Stream 来例如有效地构建 JSON 数组,这会使图片变得非常复杂,因为您需要在编写 all 元素之前保持核心资源打开。这通常意味着编写代码将对象映射到 JSON 并手动将它们写入响应(使用例如 Jackson 的 ObjectMapperHttpServletResponse.

内存占用

虽然内存占用可能会有所改善,但这主要是因为您希望避免在映射步骤中中间创建集合和附加集合 (ResultSet -> Customer - > CustomerDTO -> JSON 对象)。已经处理的元素保证从内存中被逐出,因为它们可能由于其他原因而被保留。再次,例如在 JPA 中,您必须保持 EntityManager 打开,因为它控制着资源生命周期,因此所有元素都将绑定到 EntityManager 并一直保留到 all 元素被处理。