当前位置:网站首页>利用 Repository 中的方法解决实际问题

利用 Repository 中的方法解决实际问题

2022-06-26 10:34:00 InfoQ

作者:Damon
博客:
http://www.damon8.cn
交个朋友之猿天地 | 微服务 | 容器化 | 自动化

Repository 的返回结果有哪些

我们之前已经介绍过了 Repository 的接口,那么现在来看一下这些接口支持的返回结果有哪些,如下图所示:

null
打开 SimpleJpaRepository 直接看它的 Structure 就可以知道,它实现的方法,以及父类接口的方法和返回类型包括:Optional、Iterable、List、Page、Long、Boolean、Entity 对象等,而实际上支持的返回类型还要多一些。

由于 Repository 里面支持 Iterable,所以其实 java 标准的 List、Set 都可以作为返回结果,并且也会支持其子类,Spring Data 里面定义了一个特殊的子类 Steamable,Streamable 可以替代 Iterable 或任何集合类型。它还提供了方便的方法来访问 Stream,可以直接在元素上进行 ….filter(…) 和 ….map(…) 操作,并将 Streamable 连接到其他元素。我们看个关于 UserRepository 直接继承 JpaRepository 的例子。

public interface UserRepository extends JpaRepository<User,Long> {

}

还用之前的 UserRepository 类,在测试类里面做如下调用:

User user = userRepository.save(User.builder().name(&quot;jackxx&quot;).email(&quot;[email protected]&quot;).sex(&quot;man&quot;).address(&quot;shanghai&quot;).build());

Assert.assertNotNull(user);

Streamable<User> userStreamable = userRepository.findAll(PageRequest.of(0,10)).and(User.builder().name(&quot;jack222&quot;).build());

userStreamable.forEach(System.out::println);

然后我们就会得到如下输出:

User(id=1, name=jackxx, [email protected], sex=man, address=shanghai)

User(id=null, name=jack222, email=null, sex=null, address=null)

这个例子 Streamable
<User>
&nbsp;userStreamable,实现了 Streamable 的返回结果,如果想自定义方法,可以进行如下操作。
自定义 Streamable
官方给我们提供了自定义 Streamable 的方法,不过在实际工作中很少出现要自定义保证结果类的情况,在这里我简单介绍一下方法,看如下例子:

class Product { (1)

 MonetaryAmount getPrice() { … }

}

@RequiredArgConstructor(staticName = &quot;of&quot;)

class Products implements Streamable<Product> { (2)

 private Streamable<Product> streamable;

 public MonetaryAmount getTotal() { (3)

 return streamable.stream() //

 .map(Priced::getPrice)

 .reduce(Money.of(0), MonetaryAmount::add);

 }

}

interface ProductRepository implements Repository<Product, Long> {

 Products findAllByDescriptionContaining(String text); (4)

}

以上四个步骤介绍了自定义 Streamable 的方法,分别为:

(1)Product 实体,公开 API 以访问产品价格。

(2)Streamable
<Product>
&nbsp;的包装类型可以通过 Products.of(…) 构造(通过 Lombok 注解创建的工厂方法)。

(3)包装器类型在 Streamable
<Product>
&nbsp;上公开了计算新值的其他 API。

(4)可以将包装器类型直接用作查询方法返回类型。无须返回 Stremable
<Product>
&nbsp;并将其手动包装在存储库 Client 端中。

通过以上例子你就可以做到自定义 Streamable,其原理很简单,就是实现Streamable接口,自己定义自己的实现类即可。我们也可以看下源码 QueryExecutionResultHandler 里面是否有 Streamable 子类的判断,来支持自定义 Streamable,关键源码如下:

null
通过源码你会发现 Streamable 为什么生效,下面来看看常见的集合类的返回实现。
返回结果类型 List/Stream/Page/Slice
在实际开发中,我们如何返回 List/Stream/Page/Slice 呢?

首先,新建我们的 UserRepository:

package com.example.jpa.example1;

import org.springframework.data.domain.Pageable;

import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.data.jpa.repository.Query;

import java.util.stream.Stream;

public interface UserRepository extends JpaRepository<User,Long> {

 //自定义一个查询方法,返回Stream对象,并且有分页属性

 @Query(&quot;select u from User u&quot;)

 Stream<User> findAllByCustomQueryAndStream(Pageable pageable);

 //测试Slice的返回结果

 @Query(&quot;select u from User u&quot;)

 Slice<User> findAllByCustomQueryAndSlice(Pageable pageable);

}

然后,修改一下我们的测试用例类,如下,验证一下结果:

package com.example.jpa.example1;

import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.assertj.core.util.Lists;

import org.junit.Assert;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.PageRequest;

import org.springframework.data.domain.Slice;

import org.springframework.data.util.Streamable;

import java.util.List;

import java.util.stream.Stream;

@DataJpaTest

public class UserRepositoryTest {

 @Autowired

 private UserRepository userRepository;

 @Test

 public void testSaveUser() throws JsonProcessingException {

 //我们新增7条数据方便测试分页结果

 userRepository.save(User.builder().name(&quot;jack1&quot;).email(&quot;[email protected]&quot;).sex(&quot;man&quot;).address(&quot;shanghai&quot;).build());

 userRepository.save(User.builder().name(&quot;jack2&quot;).email(&quot;[email protected]&quot;).sex(&quot;man&quot;).address(&quot;shanghai&quot;).build());

 userRepository.save(User.builder().name(&quot;jack3&quot;).email(&quot;[email protected]&quot;).sex(&quot;man&quot;).address(&quot;shanghai&quot;).build());

 userRepository.save(User.builder().name(&quot;jack4&quot;).email(&quot;[email protected]&quot;).sex(&quot;man&quot;).address(&quot;shanghai&quot;).build());

 userRepository.save(User.builder().name(&quot;jack5&quot;).email(&quot;[email protected]&quot;).sex(&quot;man&quot;).address(&quot;shanghai&quot;).build());

 userRepository.save(User.builder().name(&quot;jack6&quot;).email(&quot;[email protected]&quot;).sex(&quot;man&quot;).address(&quot;shanghai&quot;).build());

 userRepository.save(User.builder().name(&quot;jack7&quot;).email(&quot;[email protected]&quot;).sex(&quot;man&quot;).address(&quot;shanghai&quot;).build());

 //我们利用ObjectMapper将我们的返回结果Json to String

 ObjectMapper objectMapper = new ObjectMapper();

 //返回Stream类型结果(1)

 Stream<User> userStream = userRepository.findAllByCustomQueryAndStream(PageRequest.of(1,3));

 userStream.forEach(System.out::println);

 //返回分页数据(2)

 Page<User> userPage = userRepository.findAll(PageRequest.of(0,3));

 System.out.println(objectMapper.writeValueAsString(userPage));

 //返回Slice结果(3)

 Slice<User> userSlice = userRepository.findAllByCustomQueryAndSlice(PageRequest.of(0,3));

 System.out.println(objectMapper.writeValueAsString(userSlice));

 //返回List结果(4)

 List<User> userList = userRepository.findAllById(Lists.newArrayList(1L,2L));

 System.out.println(objectMapper.writeValueAsString(userList));

 }

}

这个时候我们分别看下四种测试结果:

第一种:通过
Stream
<User>
取第二页的数据,得到结果如下:

User(id=4, name=jack4, [email protected], sex=man, address=shanghai)

User(id=5, name=jack5, [email protected], sex=man, address=shanghai)

User(id=6, name=jack6, [email protected], sex=man, address=shanghai)

Spring Data 的支持可以通过使用 Java 8 Stream 作为返回类型来逐步处理查询方法的结果。
需要注意的是:流的关闭问题
,try catch 是一种常用的关闭方法,如下所示:

Stream<User> stream;

try {

 stream = repository.findAllByCustomQueryAndStream()

 stream.forEach(…);

} catch (Exception e) {

 e.printStackTrace();

} finally {

 if (stream!=null){

 stream.close();

 }

}

第二种:返回 Page
<User>
&nbsp;的分页数据结果,如下所示:

{

 &quot;content&quot;:[

 {

 &quot;id&quot;:1,

 &quot;name&quot;:&quot;jack1&quot;,

 &quot;email&quot;:&quot;[email protected]&quot;,

 &quot;sex&quot;:&quot;man&quot;,

 &quot;address&quot;:&quot;shanghai&quot;

 },

 {

 &quot;id&quot;:2,

 &quot;name&quot;:&quot;jack2&quot;,

 &quot;email&quot;:&quot;[email protected]&quot;,

 &quot;sex&quot;:&quot;man&quot;,

 &quot;address&quot;:&quot;shanghai&quot;

 },

 {

 &quot;id&quot;:3,

 &quot;name&quot;:&quot;jack3&quot;,

 &quot;email&quot;:&quot;[email protected]&quot;,

 &quot;sex&quot;:&quot;man&quot;,

 &quot;address&quot;:&quot;shanghai&quot;

 }

 ],

 &quot;pageable&quot;:{

 &quot;sort&quot;:{

 &quot;sorted&quot;:false,

 &quot;unsorted&quot;:true,

 &quot;empty&quot;:true

 },

 &quot;pageNumber&quot;:0,当前页码

 &quot;pageSize&quot;:3,页码大小

 &quot;offset&quot;:0,偏移量

 &quot;paged&quot;:true,是否分页了

 &quot;unpaged&quot;:false

 },

 &quot;totalPages&quot;:3,一共有多少页

 &quot;last&quot;:false,是否是到最后

 &quot;totalElements&quot;:7,一共多少调数

 &quot;numberOfElements&quot;:3,当前数据下标

 &quot;sort&quot;:{

 &quot;sorted&quot;:false,

 &quot;unsorted&quot;:true,

 &quot;empty&quot;:true

 },

 &quot;size&quot;:3,当前content大小

 &quot;number&quot;:0,当前页面码的索引

 &quot;first&quot;:true,是否是第一页

 &quot;empty&quot;:false是否有数据

}

这里我们可以看到 Page
<User>
&nbsp;返回了第一个页的数据,并且告诉我们一共有三个部分的数据:

  • content
    :数据的内容,现在指 User 的 List 3 条。
  • pageable
    :分页数据,包括排序字段是什么及其方向、当前是第几页、一共多少页、是否是最后一条等。
  • 当前数据的描述
    :“size”:3,当前 content 大小;“number”:0,当前页面码的索引;&nbsp; “first”:true,是否是第一页;“empty”:false,是否没有数据。

通过这三部分数据我们可以知道要查数的分页信息。我们接着看第三种测试结果。

第三种:返回 Slice
<User>
&nbsp;结果,如下所示:

{

 &quot;content&quot;:[

 {

 &quot;id&quot;:4,

 &quot;name&quot;:&quot;jack4&quot;,

 &quot;email&quot;:&quot;[email protected]&quot;,

 &quot;sex&quot;:&quot;man&quot;,

 &quot;address&quot;:&quot;shanghai&quot;

 },

 {

 &quot;id&quot;:5,

 &quot;name&quot;:&quot;jack5&quot;,

 &quot;email&quot;:&quot;[email protected]&quot;,

 &quot;sex&quot;:&quot;man&quot;,

 &quot;address&quot;:&quot;shanghai&quot;

 },

 {

 &quot;id&quot;:6,

 &quot;name&quot;:&quot;jack6&quot;,

 &quot;email&quot;:&quot;[email protected]&quot;,

 &quot;sex&quot;:&quot;man&quot;,

 &quot;address&quot;:&quot;shanghai&quot;

 }

 ],

 &quot;pageable&quot;:{

 &quot;sort&quot;:{

 &quot;sorted&quot;:false,

 &quot;unsorted&quot;:true,

 &quot;empty&quot;:true

 },

 &quot;pageNumber&quot;:1,

 &quot;pageSize&quot;:3,

 &quot;offset&quot;:3,

 &quot;paged&quot;:true,

 &quot;unpaged&quot;:false

 },

 &quot;numberOfElements&quot;:3,

 &quot;sort&quot;:{

 &quot;sorted&quot;:false,

 &quot;unsorted&quot;:true,

 &quot;empty&quot;:true

 },

 &quot;size&quot;:3,

 &quot;number&quot;:1,

 &quot;first&quot;:false,

 &quot;last&quot;:false,

 &quot;empty&quot;:false

}

这时我们发现上面的 Page 返回结果少了,那么一共有多少条结果、多少页的数据呢?我们再比较一下第二种和第三种测试结果的执行 SQL:

第二种执行的是普通的分页查询 SQL:

查询分页数据

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_ from user user0_ limit ?

计算分页数据

Hibernate: select count(user0_.id) as col_0_0_ from user user0_

第三种执行的 SQL 如下:

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_ from user user0_ limit ? offset ?

通过对比可以看出,只查询偏移量,不计算分页数据,这就是 Page 和 Slice 的主要区别。我们接着看第四种测试结果。

第四种:返回 List
<User>
&nbsp;结果如下:

[

 {

 &quot;id&quot;:1,

 &quot;name&quot;:&quot;jack1&quot;,

 &quot;email&quot;:&quot;[email protected]&quot;,

 &quot;sex&quot;:&quot;man&quot;,

 &quot;address&quot;:&quot;shanghai&quot;

 },

 {

 &quot;id&quot;:2,

 &quot;name&quot;:&quot;jack2&quot;,

 &quot;email&quot;:&quot;[email protected]&quot;,

 &quot;sex&quot;:&quot;man&quot;,

 &quot;address&quot;:&quot;shanghai&quot;

 }

]

到这里,我们可以很简单地查询出来 ID=1 和 ID=2 的数据,没有分页信息。

上面四种方法介绍了常见的多条数据返回结果的形式,单条的我就不多介绍了,相信你一看就懂,无非就是对 JDK8 的 Optional 的支持。比如支持了 Null 的优雅判断,再一个就是支持直接返回 Entity,或者一些存在 / 不存在的 Boolean 的结果和一些 count 条数的返回结果而已。

我们接下来看下 Repository 的方法是如何对异步进行支持的?
Repository 对 Feature/CompletableFuture 异步返回结果的支持:
我们可以使用 Spring 的异步方法执行Repository查询,这意味着方法将在调用时立即返回,并且实际的查询执行将发生在已提交给 Spring TaskExecutor 的任务中,比较适合定时任务的实际场景。异步使用起来比较简单,直接加@Async 注解即可,如下所示:

@Async

Future<User> findByFirstname(String firstname); (1)

@Async

CompletableFuture<User> findOneByFirstname(String firstname); (2)

@Async

ListenableFuture<User> findOneByLastname(String lastname);(3)

上述三个异步方法的返回结果,分别做如下解释:

  • 第一处:使用 java.util.concurrent.Future 的返回类型;
  • 第二处:使用 java.util.concurrent.CompletableFuture 作为返回类型;
  • 第三处:使用 org.springframework.util.concurrent.ListenableFuture 作为返回类型。

以上是对 @Async 的支持,关于实际使用需要注意以下三点内容:

  • 在实际工作中,直接在 Repository 这一层使用异步方法的场景不多,一般都是把异步注解放在 Service 的方法上面,这样的话,可以有一些额外逻辑,如发短信、发邮件、发消息等配合使用;
  • 使用异步的时候一定要配置线程池,这点切记,否则“死”得会很难看。
原网站

版权声明
本文为[InfoQ]所创,转载请带上原文链接,感谢
https://xie.infoq.cn/article/4d58a63794a88ff87b0492c57