Spring data JPA 之 Repository 中的方法返回值

4 Repository 中的方法返回值

4.1 Repository 的返回结果

我们打开 SimpleJpaRepository 可以看到常用的返回类型包括:Optional、Iterable、List、Page、Long、Boolean、Entity 对象等,⽽实际上⽀持的返回类型还要多⼀些。

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

@Test
public void test_stream() {
    User user = userRepository.save(User.builder().name("jackxx").email("123456@126.com").build());
    Assertions.assertNotNull(user);
    Streamable<User> userStreamable = userRepository.findAll(PageRequest.ofSize(10));
    userStreamable.and(User.builder().name("jack222 ").build());
    userStreamable.forEach(System.out::println);
}

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

User(id=1, name=jackxx, email=123456@126.com)
User(id=null, name=jack222 , email=null)

这个例⼦ Streamable<User> userStreamable,实现了 Streamable 的返回结果,

4.1.1 自定义 Streamable

官⽅给我们提供了⾃定义 Streamable 的⽅法,不过在实际⼯作中很少出现要⾃定义保证结果类的情况,在这⾥我简单介绍⼀下⽅法,看如下例⼦:

第一步:定义一个 Product 实体,公开 API 以访问产品的价格

class Product {
    MonetaryAmount getPrice() {}
}

第二步:Streamable<Product> 的包装类型可以通过 Products.of(…) 构造(通过 Lombok 注解创建的⼯⼚⽅法)。包装器类型在 Streamable<Product> 上公开了计算新值的其他 API。

@RequiredArgConstructor(staticName = "of")
class Products implements Streamable<Product> {
    private Streamable<Product> streamable;
    public MonetaryAmount getTotal() {
        return streamable.stream() 
            .map(Priced::getPrice)
            .reduce(Money.of(0), MonetaryAmount::add);
    }
}

第三步:可以将包装器类型直接⽤作查询⽅法返回类型,⽆须返回 Stremable<Product>

interface ProductRepository implements Repository<Product, Long> {
    Products findAllByDescriptionContaining(String text); 
}

通过以上例⼦你就可以做到⾃定义 Streamable,其原理很简单,就是实现 Streamable 接⼝,⾃⼰定义⾃⼰的实现类即可。我们也可以看下源码 QueryExecutionResultHandler ⾥⾯是否有 Streamable ⼦类的判断,来⽀持⾃定义 Streamable,关键源码如下:

在这里插入图片描述

4.1.2 返回结果类型 List/Stream/Page/Slice

在实际开发中,我们如何返回 List/Stream/Page/Slice 呢?代码如下:

public interface UserRepository extends JpaRepository<User,Long> {
    // ⾃定义⼀个查询⽅法,返回 Stream 对象,并且有分⻚属性
    @Query("select u from User u")
    Stream<User> findAllByStream(Pageable pageable);
    // 测试 Slice 的返回结果
    @Query("select u from User u")
    Slice<User> findAllBySlice(Pageable pageable);
}

然后,编写我们的测试用例:

@Test
public void test() throws JsonProcessingException {
    // 新增 7 条数据⽅便测试分⻚结果
    userRepository.save(User.builder().name("jack1").email("123456@126.com").build());
    userRepository.save(User.builder().name("jack2").email("123456@126.com").build());
    userRepository.save(User.builder().name("jack3").email("123456@126.com").build());
    userRepository.save(User.builder().name("jack4").email("123456@126.com").build());
    userRepository.save(User.builder().name("jack5").email("123456@126.com").build());
    userRepository.save(User.builder().name("jack6").email("123456@126.com").build());
    userRepository.save(User.builder().name("jack7").email("123456@126.com").build());
    // 我们利⽤ ObjectMapper 将我们的返回结果 son to String
    ObjectMapper objectMapper = new ObjectMapper();
    // 返回 Stream 类型结果
    Stream<User> userStream = userRepository.findAllByStream(PageRequest.of(1, 3));
    userStream.forEach(System.out::println);
    //返回分⻚数据
    Page<User> userPage = userRepository.findAll(PageRequest.of(0, 3));
    System.out.println(objectMapper.writeValueAsString(userPage));
    // 返回 Slice 结果
    Slice<User> userSlice = userRepository.findAllBySlice(PageRequest.of(0, 3));
    System.out.println(objectMapper.writeValueAsString(userSlice));
    // 返回 List 结果
    List<User> userList = userRepository.findAllById(Lists.newArrayList(1L, 2L));
    System.out.println(objectMapper.writeValueAsString(userList));
}

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

Stream<User> stream;
try {
    stream = repository.findAllByStream();
    stream.forEach();
} catch (Exception e) {
    e.printStackTrace();
} finally {
    if (stream!=null){
        stream.close();
    }
}

Page 和 Slice 的主要区别:Slice 只查询偏移量,不计算分⻚数据

4.1.3 Repository 对 Feature/CompletableFuture 异步返回结果的支持

我们可以使⽤ Spring 的异步⽅法执⾏ Repository 查询,这意味着⽅法将在调⽤时⽴即返回,并且实际的查询执⾏将发⽣在已提交给 Spring TaskExecutor 的任务中,⽐较适合定时任务的实际场景。异步使⽤起来⽐较简单,直接加 @Async 注解即可,如下所示:

@Async
Future<User> findByFirstname(String firstname);
@Async
CompletableFuture<User> findOneByFirstname(String firstname);
@Async
ListenableFuture<User> findOneByLastname(String lastname);

Spring Data 支持三种异步返回类型:

  • 使⽤ java.util.concurrent.Future 的返回类型;
  • 使⽤ java.util.concurrent.CompletableFuture 作为返回类型;
  • 使⽤ org.springframework.util.concurrent.ListenableFuture 作为返回类型。

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

  • 在实际⼯作中,直接在 Repository 这⼀层使⽤异步⽅法的场景不多,⼀般都是把异步注解放在 Service 的⽅法上⾯,这样的话,可以有⼀些额外逻辑,如发短信、发邮件、发消息等配合使⽤;
  • 使⽤异步的时候⼀定要配置线程池,这点切记,否则“死”得会很难看;
  • 万⼀失败我们会怎么处理?关于事务是怎么处理的呢?这种需要重点考虑的。
4.1.4 对 Reactive 的支持:Flux 与 Mono

Spring Data Common⾥⾯对React还是有⽀持的,那为什么在 JpaRespository ⾥⾯没看到有响应的返回结果⽀持呢?其实 Common ⾥⾯提供的只是接⼝,⽽JPA⾥⾯没有做相关的 Reactive 的实现,但是本身 Spring Data Common ⾥⾯对 Reactive 是⽀持的。

4.1.5 小结

打开 ResultProcessor 类的源码看⼀下⽀持的类型有哪些。

在这里插入图片描述

从上图可以看出 processResult 的时候分别对 PageQuery、Stream、Reactive 有了各⾃的判断

这⾥我们先⽤表格总结⼀下返回值,下表列出了 Spring Data JPA Query Method 机制⽀持的⽅法的返回值类型:

返回值类型描述
void不返回结果,一般是更新操作。
Map返回 Map 结构,key 是字段,value 是数据库里面字段对应的值。
PrimitivesJava 的基本类型,一般常见的是统计操作(如 long,boolean 等)。
Wrapper typesJava 的包装类。
T最多只返回一个实体,没有查询结果时返回 null。
如果超过了一个结果就会抛出 IncorrectResultSizeDataAccessException 的异常。
Iterator<T>一个迭代器。
Collection<T>一个集合。
List<T>List 及其任何子类。
Optional<T>返回 Java 8 中的 Optional 类。
查询方法的返回结果最多只能有一个。
如果超过了一个结果就会抛出 IncorrectResultSizeDataAccessException 的异常。
Stream<T>Java 8 Stream
Future<T>返回 Future。查询方法需要带有 @Async 注解,并且开启 Spring 异步执行方法的功能。
一般配合多线程使用。关系型数据库实际工作中很少用到。
CompletableFuture<T>返回 Java 8 中新引入的 CompletableFuture 类,查询方法需要带有 @Async 注解,并且开启 Spring 异步执行方法的功能。
ListenableFuture<T>返回 org.springframework.util.concurrent.ListenableFuture 类,查询方法需要带有 @Async 注解,并且开启 Spring 异步执行方法的功能。
Slice<T>返回指定大小的数据和是否还有可用数据的信息。需要方法带有 Pageable 类型的参数。
Page<T>在 Slice 的基础上附加返回分页总数等信息。需要方法带有 Pageable 类型的参数。

4.2 最常见的 DTO 返回结果的支持方法

4.2.1 Projections概念

Spring JPA 对 Projections 扩展的⽀持,我个⼈觉得这是个⾮常好的东⻄,从字⾯意思上理解就是映射,指的是和 DB 的查询结果的字段映射关系。⼀般情况下,返回的字段和 DB的查询结果的字段是⼀⼀对应的;但有的时候,需要返回⼀些指定的字段,或者返回⼀些复合型的字段,⽽不需要全部返回。

原来我们的做法是⾃⼰写各种 entity 到 view 的各种 convert 的转化逻辑,⽽ Spring Data正是考虑到了这⼀点,允许对专⽤返回类型进⾏建模,有选择地返回同⼀个实体的不同视图对象。

下⾯以 User 查询对象为例,看看怎么⾃定义返回 DTO:

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;
    private String name;
    private String email;
    private String sex;
    private String address;
}

看上⾯的原始 User 实体代码,如果我们只想返回 User 对象⾥⾯的 name 和 email,应该怎么做?下⾯我们介绍三种⽅法。

第⼀种⽅法:新建⼀张表的不同 Entity

⾸先,我们新增⼀个Entity类:通过 @Table 指向同⼀张表,这张表和 User 实例⾥⾯的表⼀样都是 user,完整内容如下:

@Entity
@Table(name = "user")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserOnlyNameEmailEntity {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;
    private String name;
    private String email;
}

然后,新增⼀个 UserOnlyNameEmailEntityRepository,做单独的查询:

public interface UserOnlyNameEmailEntityRepository extends JpaRepository<UserOnlyNameEmailEntity,Long> {
}

测试⽤例:

@Test
public void testProjections1() {
    userRepository.save(User.builder().name("jack12").email("123456@126.com").sex("man").address("shanghai").build());
    List<User> users = userRepository.findAll();
    System.out.println(users);
    UserOnlyNameEmailEntity uName = userOnlyNameEmailEntityRepository.getById(1L);
    System.out.println(uName);
}

我们看⼀下输出结果:

Hibernate: insert into user (address, email, name, sex, id) values (?, ?, ?, ?, ?)
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_
[User(id=1, name=jack12, email=123456@126.com, sex=man, address=shanghai)]
Hibernate: select useronlyna0_.id as id1_0_0_, useronlyna0_.email as email3_0_0_, useronlyna0_.name as name4_0_0_ from user useronlyna0_ where useronlyna0_.id=?
UserOnlyNameEmailEntity(id=1, name=jack12, email=123456@126.com)

上述结果可以看到,当在 user 表⾥⾯插⼊了⼀条数据,⽽ userRepository 和 userOnlyNameEmailEntityRepository 查询的都是同⼀张表 user,这种⽅式的好处是简单、⽅便,很容易可以想到;缺点就是通过两个实体都可以进⾏ update 操作,如果同⼀个项⽬⾥⾯这种实体⽐较多,到时候就容易不知道是谁更新的,从⽽导致出 bug 不好查询,实体职责划分不明确。

第⼆种⽅法:直接定义⼀个 UserOnlyNameEmailDto

⾸先,新建⼀个 DTO 类来返回我们想要的字段,它是 UserOnlyNameEmailDto,⽤来接收 name、email 两个字段的值,具体如下:

@Data
@Builder
@AllArgsConstructor
public class UserOnlyNameEmailDto {
    private String name;
    private String email;
}

其次,在 UserRepository ⾥⾯做如下⽤法:

// 测试只返回 name 和 email 的 DTO
UserOnlyNameEmailDto findByEmail(String email);

测试⽤例⾥⾯写法如下:

@Test
public void testProjections2() {
    userRepository.save(User.builder().name("jack12").email("123456@126.com").sex("man").address("shanghai").build());
    UserOnlyNameEmailDto userOnlyNameEmailDto = userRepository.findByEmail("123456@126.com");
    System.out.println(userOnlyNameEmailDto);
}

输出结果如下:

Hibernate: insert into user (address, email, name, sex, id) values (?, ?, ?, ?, ?)
Hibernate: select user0_.name as col_0_0_, user0_.email as col_1_0_ from user user0_ where user0_.email=?
UserOnlyNameEmailDto(name=jack12, email=123456@126.com)

这⾥需要注意的是,如果我们去看源码的话,看关键的 PreferredConstructorDiscoverer 类时会发现,UserDTO ⾥⾯只能有⼀个全参数构造⽅法,如下所示:
在这里插入图片描述

如上图所示,Constructor 选择的时候会帮我们做构造参数的选择,如果 DTO ⾥⾯有无参构造函数,将会优先选择无参的构造方法;如果有多个构造⽅法或者没有构造函数,则会返回 null,就会报转化错误的异常,这⼀点需要注意,异常是这样的:

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.example.jpa.example1.entity.User] to type [com.example.jpa.example1.pojo.UserOnlyNameEmailDto]

所以这种⽅式的优点就是返回的结果不需要是个实体对象,对 DB 不能进⾏除了查询之外的任何操作;缺点就是有 set ⽅法还可以改变⾥⾯的值,构造⽅法不能更改,必须全参数,这样如果是不熟悉 JPA 的新⼈操作的时候很容易引发 Bug。

第三种⽅法:返回结果是⼀个 POJO 的接口

我们再来学习⼀种返回不同字段的⽅式,这种⽅式与上⾯两种的区别是只需要定义接⼝,它的好处是只读,不需要添加构造⽅法,我们使⽤起来⾮常灵活,⼀般很难产⽣ Bug,那么它怎么实现呢?

⾸先,定义⼀个 UserOnlyName 的接⼝:

public interface UserOnlyName {
    String getName();
    String getEmail(); 
}

UserRepository 写法如下:

/**
 * 接⼝的⽅式返回DTO
 */
UserOnlyName findByAddress(String address);

测试用例如下:

@Test
public void testProjections3() {
    userRepository.save(User.builder().name("jack12").email("123456@126.com").sex("man").address("shanghai").build());
    UserOnlyName userOnlyName = userRepository.findByAddress("shanghai");
    System.out.println(userOnlyName);
}

运⾏结果如下:

Hibernate: select user0_.name as col_0_0_, user0_.email as col_1_0_ from user user0_ where user0_.address=?
org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap@50a1af86

这个时候会发现我们的 userOnlyName 接⼝成了⼀个代理对象,⾥⾯通过 Map 的格式包含了我们的要返回字段的值(如:name、email),我们⽤的时候直接调⽤接⼝⾥⾯的⽅法即可,如 userOnlyName.getName() 即可;这种⽅式的优点是接口为只读,并且语义更清晰,所以这种是⽐较推荐的做法。

源码实现是在 org.hibernate.query.criteria.internal.QueryStructure,看⼀下最终 DTO 和接⼝转化执⾏的 query 有什么不同,看下图 debug 显示的 Query 语句的位置:

在这里插入图片描述

我们可以清楚得看到两个语句的区别:

是返回 DTO 类的时候 QueryStructure ⽣成的 JPQL 语句。

select new com.example.jpa.example1.pojo.UserOnlyNameEmailDto(generatedAlias0.name, generatedAlias0.email) from User as generatedAlias0 where generatedAlias0.email=:param0

返回 DTO 接⼝形式的 query ⽣成的 JPQL。

select generatedAlias0.name, generatedAlias0.email from User as generatedAlias0 where generatedAlias0.address=:param0

两种最⼤的区别是 DTO 类需要构造⽅法 new ⼀个对象出来,这就是我们第⼆种⽅法⾥⾯需要注意的 DTO 构造函数的问题;⽽通过图⼀我们可以看到接⼝直接通过 as 别名,映射成 map 即可,⾮常灵活。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要查询`Equipment`表所有`equipmentType`的`getInjectEquipmentType()`方法返回为`Optional`的`isPresent()`方法返回值为`true`的记录的总数,您可以使用Spring Data JPA的命名规则自定义查询方法。请按照以下步骤进行操作: 首先,在`Equipment`实体类添加一个自定义查询方法,使用Spring Data JPA的命名规则来定义查询逻辑: ```java @Repository public interface EquipmentRepository extends JpaRepository<Equipment, Long> { int countByEquipmentTypeGetInjectEquipmentTypeIsPresentTrue(); } ``` 在上述代码,`countByEquipmentTypeGetInjectEquipmentTypeIsPresentTrue()`方法使用了Spring Data JPA的命名规则来定义查询逻辑。根据命名规则,Spring Data JPA会自动生成查询语句,查询`Equipment`表所有满足条件的记录并返回其总数。 然后,在您的服务类(例如,`EquipmentService`)注入`EquipmentRepository`,并调用该方法来获取满足条件的记录的总数: ```java @Service public class EquipmentService { private final EquipmentRepository equipmentRepository; public EquipmentService(EquipmentRepository equipmentRepository) { this.equipmentRepository = equipmentRepository; } public int countEquipmentsWithInjectEquipmentType() { return equipmentRepository.countByEquipmentTypeGetInjectEquipmentTypeIsPresentTrue(); } } ``` 在上述代码,`countEquipmentsWithInjectEquipmentType()`方法调用了`countByEquipmentTypeGetInjectEquipmentTypeIsPresentTrue()`方法来获取满足条件的记录的总数。 通过这种方式,您可以使用Spring Data JPA和基于命名规则的查询方法来查询`Equipment`表所有`equipmentType`的`getInjectEquipmentType()`方法返回为`Optional`的`isPresent()`方法返回值为`true`的记录的总数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值