Spring-Data数据查询方法的返回通常的是Repository管理的聚合根的一个或多个实例。但是,有时候我们只需要返回某些特定的属性,不需要全部返回,或者只返回一些复合型的字段。Spring-Data允许我们对特定的返回类型建模,以便更有选择的检索托管聚合的部分视图。

1、基于接口的投影

  查询执行引擎在运行时为返回的每个元素创建该接口的代理实例,并将调用转发到目标对象的公开方法。

  1.1、闭合投影(Closed Projections):一个投影接口,其get方法都与实体类的属性相同,被认为是一个闭合投影。如果使用闭合投影Spring-Data可以优化查询执行,因为我们知道支持投影代理所需要的所有属性。

  比如说一个Admin类如下:

  1. /**
  2. * admin实体
  3. *
  4. * @author caofanqi
  5. */
  6. @Data
  7. @Entity
  8. @Builder
  9. @Table(name = "jpa_admin")
  10. @NoArgsConstructor
  11. @AllArgsConstructor
  12. public class Admin {
  13.  
  14. @Id
  15. @GeneratedValue(strategy = GenerationType.IDENTITY)
  16. private Long id;
  17.  
  18. private String username;
  19.  
  20. private String password;
  21.  
  22. private String phone;
  23.  
  24. private LocalDate createTime;
  25.  
  26. @Embedded
  27. private Address address;
  28.  
  29. @ManyToOne
  30. @JoinColumn(name = "role_id")
  31. private Role role;
  32.  
  33. }

  Repository中的方法如下:

  1. List<Admin> findByCreateTime(LocalDate createTime);

  测试,返回Admin打印的SQL语句:

  1. Hibernate: select admin0_.id as id1_0_, admin0_.city as city2_0_, admin0_.county as county3_0_, admin0_.detailed_address as detailed4_0_, admin0_.province as province5_0_, admin0_.zip_code as zip_code6_0_, admin0_.create_time as create_t7_0_, admin0_.password as password8_0_, admin0_.phone as phone9_0_, admin0_.role_id as role_id11_0_, admin0_.username as usernam10_0_ from cfq_jpa_admin admin0_ where admin0_.create_time=?

  我们不想取出Admin中的全部属性值,那怎么办呢?我们可以新建一个投影接口,提供自己需要属性的get方法,如下,我们只想要username

  1. /**
  2. * username投影
  3. * @author caofanqi
  4. */
  5. public interface UsernameOnly {
  6. String getUsername();
  7. }

  修改Repository方法返回值,测试返回UsernameOnly打印的SQL语句:

  1. Hibernate: select admin0_.username as col_0_0_ from cfq_jpa_admin admin0_ where admin0_.create_time=?

  可见对sql语句进行了优化。那么如果我们想要返回username和所在city呢?

  投影可以嵌套使用。例如还希望包含一些地址信息,请为此创建一个投影接口,并从getAddress()的声明中返回该接口。投影接口如下:

  1. /**
  2. * 地址投影,只返回city
  3. * @author caofanqi
  4. */
  5. public interface AddressCity {
  6. String getCity();
  7. }
  8.  
  9. /**
  10. * 想返回 username 和 所在城市的投影
  11. * @author caofanqi
  12. */
  13. public interface AdminUsernameAndCity {
  14.  
  15. String getUsername();
  16.  
  17. AddressCity getAddress();
  18.  
  19. }

  修改Repository方法返回值,测试返回AdminUsernameAndCity打印的SQL语句:除了username外,select后,还有address中的属性,做了部分优化

  1. Hibernate: select admin0_.username as col_0_0_, admin0_.city as col_1_0_, admin0_.county as col_1_1_, admin0_.detailed_address as col_1_2_, admin0_.province as col_1_3_, admin0_.zip_code as col_1_4_ from cfq_jpa_admin admin0_ where admin0_.create_time=?

  1.2、开放投影(Open Projections):投影接口中的get方法也可以使用@Value注释计算新值。

  1.2.1、比如说我们要返回username和address的属性拼好的地址投影接口如下:

  1. /**
  2. * username和全地址拼接投影
  3. * @author caofanqi
  4. */
  5. public interface AdminUsernameAndFullAddress {
  6.  
  7. String getUsername();
  8.  
  9. @Value("#{target.address.province + ' ' + target.address.city + ' ' + target.address.county + ' ' + target.address.detailedAddress}")
  10. String getFullAddress();
  11. }

  修改Repository方法返回值,测试,返回AdminUsernameAndFullAddress打印的SQL语句如下:

  1. Hibernate: select admin0_.id as id1_0_, admin0_.city as city2_0_, admin0_.county as county3_0_, admin0_.detailed_address as detailed4_0_, admin0_.province as province5_0_, admin0_.zip_code as zip_code6_0_, admin0_.create_time as create_t7_0_, admin0_.password as password8_0_, admin0_.phone as phone9_0_, admin0_.role_id as role_id11_0_, admin0_.username as usernam10_0_ from cfq_jpa_admin admin0_ where admin0_.create_time=?

  我们发现SQL语句并没有优化,那是因为target变量中提供了支持投影的实体。使用@Value的投影接口是一个开放的投影。在这种情况下,Spring-Data不能进行查询优化,因为spel表达式可以使用实体的任何属性。

  1.2.2、@Value中使用的表达式不要太复杂,要避免字符串变量编程。对于非常简单的表达式,可以选择使用java8中引入的接口默认方法。

  1. /**
  2. * 使用默认接口方法返回全地址拼接路径投影
  3. * @author caofanqi
  4. */
  5. public interface AdminUsernameAndFullAddressWithJava8 {
  6.  
  7. String getUsername();
  8.  
  9. /**
  10. * 要提供address的get方法供使用。
  11. */
  12. Address getAddress();
  13.  
  14. default String getFullAddress() {
  15. return getAddress().getProvince().concat(" ").concat(getAddress().getCity()).concat(" ").concat(getAddress().getCounty())
  16. .concat(" ").concat(getAddress().getDetailedAddress());
  17. }
  18.  
  19. }

  修改Repository方法返回值,测试返回AdminUsernameAndFullAddressWithJava8打印的SQL语句:进行了部分优化,没有把admin中全部的属性都查

  1. Hibernate: select admin0_.username as col_0_0_, admin0_.city as col_1_0_, admin0_.county as col_1_1_, admin0_.detailed_address as col_1_2_, admin0_.province as col_1_3_, admin0_.zip_code as col_1_4_ from cfq_jpa_admin admin0_ where admin0_.create_time=?

  1.2.3、但是java8方式要求能够完全基于投影接口上公开的其他get方法来实现逻辑。更灵活的方法是选择在Spring Bean中实现自定义逻辑,然后从spel表达式中调用该自定义逻辑。

  1. /**
  2. * @author caofanqi
  3. */
  4. @Component
  5. public class MyAdminBean {
  6.  
  7. public String getFullAddress(Admin admin) {
  8. Address address = admin.getAddress();
  9. return address.getProvince().concat(" ").concat(address.getCity()).concat(" ").concat(address.getCounty())
  10. .concat(" ").concat(address.getDetailedAddress());
  11. }
  12.  
  13. }
  14.  
  15. /**
  16. * 使用spring bean的方式的投影
  17. */
  18. public interface AdminUsernameAndFullAddressWithSpringBean {
  19.  
  20. String getUsername();
  21.  
  22. @Value("#{@myAdminBean.getFullAddress(target)}")
  23. String getFullAddress();
  24.  
  25. }

  修改Repository方法返回值,测试返回AdminUsernameAndFullAddressWithSpringBean打印的SQL语句:因为使用了target,所以没有进行优化。

  1. Hibernate: select admin0_.id as id1_0_, admin0_.city as city2_0_, admin0_.county as county3_0_, admin0_.detailed_address as detailed4_0_, admin0_.province as province5_0_, admin0_.zip_code as zip_code6_0_, admin0_.create_time as create_t7_0_, admin0_.password as password8_0_, admin0_.phone as phone9_0_, admin0_.role_id as role_id11_0_, admin0_.username as usernam10_0_ from cfq_jpa_admin admin0_ where admin0_.create_time=?

  spel也可以使用方法中的参数值。方法参数可通过名为args的Object数组中获得。

  1. /**
  2. * spel使用方法中的参数值投影
  3. * @author caofanqi
  4. */
  5. public interface PrefixUsername {
  6.  
  7. String getUsername();
  8.  
  9. @Value("#{args[0] + '' + target.username + '!'}")
  10. String getPrefixUsername(String prefix);
  11.  
  12. }

2、基于类的投影DTO

  定义投影的另一种方是使用值类型DTO(数据传输对象),该DTO持有需要检索的属性。DTO投影的使用方式与接口投影完全相同,只是不会发生代理,也不能用嵌套投影。要加载的字段由公开的构造方法的参数名确定。使用lombok的@Value注解来简化DTO编写。

  比如说只想返回用户名使用DTO的方式的投影,如下:

  1. import lombok.Value;
  2.  
  3. /**
  4. * 使用DTO的方式返回用户名,需要构造函数,我们使用lombok的@Value方法来简化代码
  5. * @author caofanqi
  6. */
  7. @Value
  8. public class UsernameDTO {
  9.  
  10. private String username;
  11.  
  12. }

  修改Repository方法返回值,测试返回UsernameDTO打印的SQL:也进行了优化

  1. Hibernate: select admin0_.username as col_0_0_ from cfq_jpa_admin admin0_ where admin0_.create_time=?

3、动态投影

  到目前为止,我们使用的投影类型作为集合的返回类型或元素类型。如果我们想要在调用时才确定投影的类型呢,这也是可以的。

  Repository方法改造为如下:

  1. /**
  2. * 动态返回投影,type可以是实体,接口投影,DTO投影
  3. */
  4. <T> List<T> findByCreateTime(LocalDate createTime, Class<T> type);

  调用时,动态确定返回投影:

  1. @Test
  2. void findByCreateTime2(){
  3. //返回实体Admin
  4. List<Admin> list1 = adminRepository.findByCreateTime(LocalDate.of(2019, 11, 11), Admin.class);
  5. list1.forEach( System.out::println);
  6.  
  7. System.out.println("===================");
  8.  
  9. //返回接口投影
  10. List<UsernameOnly> list2 = adminRepository.findByCreateTime(LocalDate.of(2019, 11, 11), UsernameOnly.class);
  11. list2.forEach(u -> System.out.println(u.getUsername()));
  12.  
  13. System.out.println("===================");
  14.  
  15. //返回DTO投影
  16. List<UsernameDTO> list3 = adminRepository.findByCreateTime(LocalDate.of(2019, 11, 21), UsernameDTO.class);
  17. list3.forEach(u -> System.out.println(u.getUsername()));
  18. }

4、投影支持分页排序,和返回Optional等。

  1. /**
  2. * 支持分页
  3. * @param createTime
  4. * @param type
  5. * @param pageable
  6. * @param <T>
  7. * @return
  8. */
  9. <T> Page<T> findByCreateTime(LocalDate createTime, Class<T> type, Pageable pageable);
  10.  
  11. @Test
  12. void findByCreateTimeWithPage(){
  13. Page<AdminUsernameAndAddressDTO> page = adminRepository.findByCreateTime(LocalDate.of(2019, 11, 21), AdminUsernameAndAddressDTO.class, PageRequest.of(1, 2, Sort.Direction.DESC,"username"));
  14. System.out.println(page.getTotalElements());
  15. System.out.println(page.getTotalPages());
  16. System.out.println(page.getNumberOfElements());
  17. System.out.println(page.getContent());
  18. }

5、投影与@Query的使用

  有时,我们需要多表关联,使用一些分组函数进行求职计算等,我们要使用投影来接收返回值,提高我们代码的可读性,而不是使用Objec[],Map等去接收。

  5.1、举个例子,我们想知道每个角色名称对应的管理员数量和平均年龄,我们创建接口投影如下:

  1. /**
  2. * 角色名称,admin个数count ,admin平均年龄 投影
  3. * @author caofanqi
  4. */
  5. public interface RoleNameAndAdminCountAndAgeAvg {
  6.  
  7. String getRoleName();
  8.  
  9. Long getAdminCount();
  10.  
  11. Double getAgeAvg();
  12.  
  13. }

  可以使用JPQL或原生SQL进行查询:

  1. /**
  2. * JPQL 使用投影
  3. */
  4. @Query(value = "select r.roleName as roleName,count(a) as adminCount , avg(a.age) as ageAvg from Role r inner join Admin a on r = a.role group by r.roleName ")
  5. List<RoleNameAndAdminCountAndAgeAvg> findRoleNameAndAdminCountAndAgeAvgWithJPQL();
  6.  
  7. /**
  8. * 原生SQL 使用投影
  9. */
  10. @Query(value = "SELECT r.role_name AS roleName,COUNT(*) AS adminCount,AVG(a.age) AS ageAvg FROM cfq_jpa_role r INNER JOIN cfq_jpa_admin a ON r.id = a.role_id GROUP BY r.role_name ", nativeQuery = true)
  11. List<RoleNameAndAdminCountAndAgeAvg> findRoleNameAndAdminCountAndAgeAvgWithSQL();

  测试接口投影接收JPQL返回值:

  1. @Test
  2. void findRoleNameAndAdminCountAndAgeAvgWithJPQL(){
  3. List<RoleNameAndAdminCountAndAgeAvg> roleNameAndAdminCountAndAgeAvgWithJPQL = roleRepository.findRoleNameAndAdminCountAndAgeAvgWithJPQL();
  4. roleNameAndAdminCountAndAgeAvgWithJPQL.forEach(r -> System.out.println(r.getRoleName() + " : " + r.getAdminCount() + " : " + r.getAgeAvg()));
  5. }

  JPQL控制台打印如下:

  1. Hibernate: select role0_.role_name as col_0_0_, count(admin1_.id) as col_1_0_, avg(admin1_.age) as col_2_0_ from cfq_jpa_role role0_ inner join cfq_jpa_admin admin1_ on (role0_.id=admin1_.role_id) group by role0_.role_name
  2. 普通管理员 : 3 : 26.0
  3. 超级管理员 : 2 : 23.5

  测试接口投影接收SQL返回值:

  1. @Test
  2. void findRoleNameAndAdminCountAndAgeAvgWithSQL(){
  3. List<RoleNameAndAdminCountAndAgeAvg> roleNameAndAdminCountAndAgeAvgWithSQL = roleRepository.findRoleNameAndAdminCountAndAgeAvgWithSQL();
  4. roleNameAndAdminCountAndAgeAvgWithSQL.forEach(r -> System.out.println(r.getRoleName() + " : " + r.getAdminCount() + " : " + r.getAgeAvg()));
  5. }

  SQL控制台打印如下:

  1. Hibernate: SELECT r.role_name AS roleName,COUNT(*) AS adminCount,AVG(a.age) AS ageAvg FROM cfq_jpa_role r INNER JOIN cfq_jpa_admin a ON r.id = a.role_id GROUP BY r.role_name
  2. 普通管理员 : 3 : 26.0
  3. 超级管理员 : 2 : 23.5

  

  5.2、使用DTO投影来进行接收时,要使用如下方式:

  1. import lombok.Value;
  2.  
  3. /**
  4. * 角色名称,对应的管理管个数,管理员平均年龄
  5. * @author caofanqi
  6. */
  7. @Value
  8. public class RoleNameAndAdminCountAndAgeAvgDTO {
  9.  
  10. private String roleName;
  11.  
  12. private Long adminCount;
  13.  
  14. private Double ageAvg;
  15.  
  16. }

  Repository方法:

  1. /**
  2. * 使用DTO投影接收JPQL查询结果,如果不是实体本身的属性,要使用如下方式
  3. */
  4. @Query(value = "select new cn.caofanqi.study.studyspringdatajpa.pojo.domain.projections.RoleNameAndAdminCountAndAgeAvgDTO(r.roleName ,count(a), avg(a.age)) from Role r inner join Admin a on r = a.role group by r.roleName")
  5. List<RoleNameAndAdminCountAndAgeAvgDTO> findRoleNameAndAdminCountAndAgeAvgWithDTO();

  测试用例:

  1. @Test
  2. void findRoleNameAndAdminCountAndAgeAvgWithDTO(){
  3. List<RoleNameAndAdminCountAndAgeAvgDTO> roleNameAndAdminCountAndAgeAvgWithDTO = roleRepository.findRoleNameAndAdminCountAndAgeAvgWithDTO();
  4. roleNameAndAdminCountAndAgeAvgWithDTO.forEach(r -> System.out.println(r.getRoleName() + " : " + r.getAdminCount() + " : " + r.getAgeAvg()));
  5. }

  控制台打印:

  1. Hibernate: select role0_.role_name as col_0_0_, count(admin1_.id) as col_1_0_, avg(admin1_.age) as col_2_0_ from cfq_jpa_role role0_ inner join cfq_jpa_admin admin1_ on (role0_.id=admin1_.role_id) group by role0_.role_name
  2. 普通管理员 : 3 : 26.0
  3. 超级管理员 : 2 : 23.5

  DTO投影接收原生SQL返回就比较麻烦了,如下:

  实体类中添加如下:

  1. @NamedNativeQueries({
  2. @NamedNativeQuery(name = "Role.findRoleNameAndAdminCountAndAgeAvgDTOWithSQL",
  3. query = "SELECT r.role_name AS roleName,COUNT(*) AS adminCount,AVG(a.age) AS ageAvg FROM cfq_jpa_role r INNER JOIN cfq_jpa_admin a ON r.id = a.role_id GROUP BY r.role_name",
  4. resultSetMapping = "roleNameAndAdminCountAndAgeAvgDTO")})
  5. @SqlResultSetMapping(
  6. name = "roleNameAndAdminCountAndAgeAvgDTO",
  7. classes = @ConstructorResult(targetClass = RoleNameAndAdminCountAndAgeAvgDTO.class,
  8. columns = {
  9. @ColumnResult(name = "roleName", type = String.class),
  10. @ColumnResult(name = "adminCount", type = Long.class),
  11. @ColumnResult(name = "ageAvg", type = Double.class)
  12. }))

  Repository接口方法如下:

  1. /**
  2. * 原生SQL 使用DTO投影,需要@NamedNativeQuery、@SqlResultSetMapping、@Query(nativeQuery = true)注解一起使用
  3. */
  4. @Query(nativeQuery = true)
  5. List<RoleNameAndAdminCountAndAgeAvgDTO> findRoleNameAndAdminCountAndAgeAvgDTOWithSQL();

  测试及控制台打印

  1. @Test
  2. void findRoleNameAndAdminCountAndAgeAvgDTOWithSQL(){
  3. List<RoleNameAndAdminCountAndAgeAvgDTO> roleNameAndAdminCountAndAgeAvgDTOWithSQL = roleRepository.findRoleNameAndAdminCountAndAgeAvgDTOWithSQL();
  4. roleNameAndAdminCountAndAgeAvgDTOWithSQL.forEach(r -> System.out.println(r.getRoleName() + " : " + r.getAdminCount() + " : " + r.getAgeAvg()));
  5. }
  1. Hibernate: SELECT r.role_name AS roleName,COUNT(*) AS adminCount,AVG(a.age) AS ageAvg FROM cfq_jpa_role r INNER JOIN cfq_jpa_admin a ON r.id = a.role_id GROUP BY r.role_name
  2. 普通管理员 : 3 : 26.0
  3. 超级管理员 : 2 : 23.5
  1. 源码地址:https://github.com/caofanqi/study-spring-data-jpa

学习Spring-Data-Jpa(十二)---投影Projections-对查询结果的扩展的更多相关文章

  1. 【tmos】spring data jpa 创建方法名进行简单查询

    参考链接 spring data jpa 创建方法名进行简单查询:http://www.cnblogs.com/toSeeMyDream/p/6170790.html

  2. 学习Spring Data JPA

    简介 Spring Data 是spring的一个子项目,在官网上是这样解释的: Spring Data 是为数据访问提供一种熟悉且一致的基于Spring的编程模型,同时仍然保留底层数据存储的特​​殊 ...

  3. 学习-spring data jpa

    spring data jpa对照表 Keyword Sample JPQL snippet And findByLastnameAndFirstname - where x.lastname = ? ...

  4. spring data jpa 创建方法名进行简单查询

    版权声明:本文为博主原创文章,未经博主允许不得转载. spring data jpa 可以通过在接口中按照规定语法创建一个方法进行查询,spring data jpa 基础接口中,如CrudRepos ...

  5. Spring data JPA中使用Specifications动态构建查询

    有时我们在查询某个实体的时候,给定的条件是不固定的,这是我们就需要动态 构建相应的查询语句,在JPA2.0中我们可以通过Criteria接口查询,JPA criteria查询.相比JPQL,其优势是类 ...

  6. Spring Data JPA,一种动态条件查询的写法

    我们在使用SpringData JPA框架时,进行条件查询,如果是固定条件的查询,我们可以使用符合框架规则的自定义方法以及@Query注解实现. 如果是查询条件是动态的,框架也提供了查询接口. Jpa ...

  7. JavaEE 之 Spring Data JPA(二)

    1.JPQL a.定义:Java持久化查询语言(JPQL)是一种可移植的查询语言,旨在以面向对象表达式语言的表达式,将SQL语法和简单查询语义绑定在一起·使用这种语言编写的查询是可移植的,可以被编译成 ...

  8. Spring Data JPA 复杂/多条件组合分页查询

    推荐视频: http://www.icoolxue.com/album/show/358 public Map<String, Object> getWeeklyBySearch(fina ...

  9. 序列化表单为json对象,datagrid带额外参提交一次查询 后台用Spring data JPA 实现带条件的分页查询 多表关联查询

    查询窗口中可以设置很多查询条件 表单中输入的内容转为datagrid的load方法所需的查询条件向原请求地址再次提出新的查询,将结果显示在datagrid中 转换方法看代码注释 <td cols ...

  10. Spring Data Jpa使用@Query注解实现模糊查询(LIKE关键字)

    /** * * @param file_name 传入参数 * @return */ @Query(value = "select * from user where name LIKE C ...

随机推荐

  1. Go语言【学习】defer和逃逸分析

    defer 什么是defer? defer是Go语言的一中用于注册延迟调用的机制,使得函数活语句可以再当前函数执行完毕后执行 为什么需要defer? Go语言提供的语法糖,减少资源泄漏的发生 如何使用 ...

  2. 【转】ISE——完整工程的建立

    FPGA公司主要是两个Xilinx和Altera(现intel PSG),我们目前用的ISE是Xilinx的开发套件,现在ISE更新到14.7已经不更新了,换成了另一款开发套件Vivado,也是Xil ...

  3. python内存机制与垃圾回收、调优手段

    目录 一.python的内存机制 二.python的垃圾回收 1. 引用计数 1.1 原理: 1.2 优缺点: 1.4 两种情况: 2. 标记清除 2.1 原理: 2.2 优缺点: 3. 分代回收 3 ...

  4. 关于ABViewer的疑问解答

    很多 CAD小伙伴都对 ABViewer 这款软件不陌生吧.ABViewer 是用来处理图纸和工程文档管理的一款通用软件.可以用它来查看,编辑,转换,测量和打印DWG和其他CAD文件,以及3D模型和光 ...

  5. layui加载显示问题

    1.layui.config({ base: '../layuiadmin/' //静态资源所在路径 }).extend({ index: 'lib/index' //主入口模块 }).use(['i ...

  6. mysql主从一致性校验工具-pt

    一.环境 1.系统环境 系统 IP 主机名 说明 server_id centos6.7 MasterIP master 数据库:主 177  centos6.7 SlaveIP slave 数据库: ...

  7. python系列:一、Urllib库的基本使用

    开篇介绍: 因为我本人也是初学者,爬虫的例子大部分都是学习资料上面来的,只是自己手敲了一遍,同时加上自己的理解. 写得不好请多谅解,如果有错误之处请多赐教. 我本人的开发环境是vscode,pytho ...

  8. ajax请求处理概要

    /** *不关心参数传递与参数返回的形式. */ url = ctxPath + '/ccb/xxx '; $.get(url); $.post(url); /** * 常见形式. */ var ur ...

  9. 记录第n次网站渗透经历

    如标题所示,第x次实战获取webshell的经历是非常美好且需要记录的(毕竟开始写博客了嘛).这能够证明这一路来的学习没有白费,也应用上了该用的知识. 首先怎么说呢,某天去补天看了看漏洞,发现有一个网 ...

  10. Kotlin字节码生成机制详尽分析

    通过注解修改Kotlin的class文件名: 对于Kotlin文件在编译之后生成的class文件名默认是有一定规则的,比如: 而其实这个生成字节码的文件名称是可以被改的,之前https://www.c ...