學習Spring-Data-Jpa(十二)---投影Projections-對查詢結果的擴展


  Spring-Data數據查詢方法的返回通常的是Repository管理的聚合根的一個或多個實例。但是,有時候我們只需要返回某些特定的屬性,不需要全部返回,或者只返回一些復合型的字段。Spring-Data允許我們對特定的返回類型建模,以便更有選擇的檢索托管聚合的部分視圖。

1、基於接口的投影

  查詢執行引擎在運行時為返回的每個元素創建該接口的代理實例,並將調用轉發到目標對象的公開方法。

  1.1、閉合投影(Closed Projections):一個投影接口,其get方法都與實體類的屬性相同,被認為是一個閉合投影。如果使用閉合投影Spring-Data可以優化查詢執行,因為我們知道支持投影代理所需要的所有屬性。

  比如說一個Admin類如下:

/**
 * admin實體
 *
 * @author caofanqi
 */
@Data
@Entity
@Builder
@Table(name = "jpa_admin")
@NoArgsConstructor
@AllArgsConstructor
public class Admin {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    private String phone;

    private LocalDate createTime;

    @Embedded
    private Address address;

    @ManyToOne
    @JoinColumn(name = "role_id")
    private Role role;

}

  Repository中的方法如下:

List<Admin> findByCreateTime(LocalDate createTime);

  測試,返回Admin打印的SQL語句:

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

/**
 * username投影
 * @author caofanqi
 */
public interface UsernameOnly {
    String getUsername();
}

  修改Repository方法返回值,測試返回UsernameOnly打印的SQL語句:

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

  可見對sql語句進行了優化。那么如果我們想要返回username和所在city呢?

  投影可以嵌套使用。例如還希望包含一些地址信息,請為此創建一個投影接口,並從getAddress()的聲明中返回該接口。投影接口如下:

/**
 * 地址投影,只返回city
 * @author caofanqi
 */
public interface AddressCity {
    String getCity();
}

/**
 * 想返回 username 和 所在城市的投影
 * @author caofanqi
 */
public interface AdminUsernameAndCity {

    String getUsername();

    AddressCity getAddress();

}

  修改Repository方法返回值,測試返回AdminUsernameAndCity打印的SQL語句:除了username外,select后,還有address中的屬性,做了部分優化

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的屬性拼好的地址投影接口如下:

/**
 * username和全地址拼接投影
 * @author caofanqi
 */
public interface AdminUsernameAndFullAddress {

    String getUsername();

    @Value("#{target.address.province + ' ' + target.address.city + ' ' + target.address.county + ' ' + target.address.detailedAddress}")
    String getFullAddress();
}

  修改Repository方法返回值,測試,返回AdminUsernameAndFullAddress打印的SQL語句如下:

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中引入的接口默認方法。

/**
 * 使用默認接口方法返回全地址拼接路徑投影
 * @author caofanqi
 */
public interface AdminUsernameAndFullAddressWithJava8 {

    String getUsername();

    /**
     * 要提供address的get方法供使用。
     */
    Address getAddress();

    default String getFullAddress() {
        return getAddress().getProvince().concat(" ").concat(getAddress().getCity()).concat(" ").concat(getAddress().getCounty())
                .concat(" ").concat(getAddress().getDetailedAddress());
    }

}

  修改Repository方法返回值,測試返回AdminUsernameAndFullAddressWithJava8打印的SQL語句:進行了部分優化,沒有把admin中全部的屬性都查

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表達式中調用該自定義邏輯。

/**
 * @author caofanqi
 */
@Component
public class MyAdminBean {

    public String getFullAddress(Admin admin) {
        Address address = admin.getAddress();
        return address.getProvince().concat(" ").concat(address.getCity()).concat(" ").concat(address.getCounty())
                .concat(" ").concat(address.getDetailedAddress());
    }

}


/**
 * 使用spring bean的方式的投影
 */
public interface AdminUsernameAndFullAddressWithSpringBean {

    String getUsername();

    @Value("#{@myAdminBean.getFullAddress(target)}")
    String getFullAddress();

}

  修改Repository方法返回值,測試返回AdminUsernameAndFullAddressWithSpringBean打印的SQL語句:因為使用了target,所以沒有進行優化。

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數組中獲得。

/**
 * spel使用方法中的參數值投影
 * @author caofanqi
 */
public interface PrefixUsername {

    String getUsername();

    @Value("#{args[0] + '' + target.username + '!'}")
    String getPrefixUsername(String prefix);

}

 

2、基於類的投影DTO

  定義投影的另一種方是使用值類型DTO(數據傳輸對象),該DTO持有需要檢索的屬性。DTO投影的使用方式與接口投影完全相同,只是不會發生代理,也不能用嵌套投影。要加載的字段由公開的構造方法的參數名確定。使用lombok的@Value注解來簡化DTO編寫。

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

import lombok.Value;

/**
 * 使用DTO的方式返回用戶名,需要構造函數,我們使用lombok的@Value方法來簡化代碼
 * @author caofanqi
 */
@Value
public class UsernameDTO {

    private String username;

}

  修改Repository方法返回值,測試返回UsernameDTO打印的SQL:也進行了優化

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

 

3、動態投影

  到目前為止,我們使用的投影類型作為集合的返回類型或元素類型。如果我們想要在調用時才確定投影的類型呢,這也是可以的。

  Repository方法改造為如下:

    /**
     *  動態返回投影,type可以是實體,接口投影,DTO投影
     */
    <T> List<T> findByCreateTime(LocalDate createTime, Class<T> type);

  調用時,動態確定返回投影:

    @Test
    void findByCreateTime2(){
        //返回實體Admin
        List<Admin> list1 = adminRepository.findByCreateTime(LocalDate.of(2019, 11, 11), Admin.class);
        list1.forEach( System.out::println);

        System.out.println("===================");

        //返回接口投影
        List<UsernameOnly> list2 = adminRepository.findByCreateTime(LocalDate.of(2019, 11, 11), UsernameOnly.class);
        list2.forEach(u -> System.out.println(u.getUsername()));

        System.out.println("===================");

        //返回DTO投影
        List<UsernameDTO> list3 = adminRepository.findByCreateTime(LocalDate.of(2019, 11, 21), UsernameDTO.class);
        list3.forEach(u -> System.out.println(u.getUsername()));
    }

 

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

    /**
     * 支持分頁
     * @param createTime
     * @param type
     * @param pageable
     * @param <T>
     * @return
     */
    <T> Page<T> findByCreateTime(LocalDate createTime, Class<T> type, Pageable pageable);


    @Test
    void findByCreateTimeWithPage(){
        Page<AdminUsernameAndAddressDTO> page = adminRepository.findByCreateTime(LocalDate.of(2019, 11, 21), AdminUsernameAndAddressDTO.class, PageRequest.of(1, 2, Sort.Direction.DESC,"username"));
        System.out.println(page.getTotalElements());
        System.out.println(page.getTotalPages());
        System.out.println(page.getNumberOfElements());
        System.out.println(page.getContent());
    }

 

5、投影與@Query的使用

  有時,我們需要多表關聯,使用一些分組函數進行求職計算等,我們要使用投影來接收返回值,提高我們代碼的可讀性,而不是使用Objec[],Map等去接收。

  5.1、舉個例子,我們想知道每個角色名稱對應的管理員數量和平均年齡,我們創建接口投影如下:

/**
 * 角色名稱,admin個數count ,admin平均年齡 投影
 * @author caofanqi
 */
public interface RoleNameAndAdminCountAndAgeAvg {

    String getRoleName();

    Long getAdminCount();

    Double getAgeAvg();

}

  可以使用JPQL或原生SQL進行查詢:

    /**
     * JPQL 使用投影
     */
    @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 ")
    List<RoleNameAndAdminCountAndAgeAvg> findRoleNameAndAdminCountAndAgeAvgWithJPQL();

    /**
     * 原生SQL 使用投影
     */
    @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)
    List<RoleNameAndAdminCountAndAgeAvg> findRoleNameAndAdminCountAndAgeAvgWithSQL();

  測試接口投影接收JPQL返回值:

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

  JPQL控制台打印如下:

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
普通管理員 : 3 : 26.0
超級管理員 : 2 : 23.5

  測試接口投影接收SQL返回值:

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

  SQL控制台打印如下:

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 
普通管理員 : 3 : 26.0
超級管理員 : 2 : 23.5

  

  5.2、使用DTO投影來進行接收時,要使用如下方式:

import lombok.Value;

/**
 * 角色名稱,對應的管理管個數,管理員平均年齡
 * @author caofanqi
 */
@Value
public class RoleNameAndAdminCountAndAgeAvgDTO {

    private String roleName;

    private Long adminCount;

    private Double ageAvg;

}

  Repository方法:

    /**
     * 使用DTO投影接收JPQL查詢結果,如果不是實體本身的屬性,要使用如下方式
     */
    @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")
    List<RoleNameAndAdminCountAndAgeAvgDTO> findRoleNameAndAdminCountAndAgeAvgWithDTO();

  測試用例:

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

  控制台打印:

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
普通管理員 : 3 : 26.0
超級管理員 : 2 : 23.5

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

  實體類中添加如下:

@NamedNativeQueries({
        @NamedNativeQuery(name = "Role.findRoleNameAndAdminCountAndAgeAvgDTOWithSQL",
                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",
                resultSetMapping = "roleNameAndAdminCountAndAgeAvgDTO")})
@SqlResultSetMapping(
        name = "roleNameAndAdminCountAndAgeAvgDTO",
        classes = @ConstructorResult(targetClass = RoleNameAndAdminCountAndAgeAvgDTO.class,
                columns = {
                        @ColumnResult(name = "roleName", type = String.class),
                        @ColumnResult(name = "adminCount", type = Long.class),
                        @ColumnResult(name = "ageAvg", type = Double.class)
                }))

  Repository接口方法如下:

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

  測試及控制台打印

    @Test
    void findRoleNameAndAdminCountAndAgeAvgDTOWithSQL(){
        List<RoleNameAndAdminCountAndAgeAvgDTO> roleNameAndAdminCountAndAgeAvgDTOWithSQL = roleRepository.findRoleNameAndAdminCountAndAgeAvgDTOWithSQL();
        roleNameAndAdminCountAndAgeAvgDTOWithSQL.forEach(r -> System.out.println(r.getRoleName() + " : " + r.getAdminCount() + " : " + r.getAgeAvg()));
    }
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
普通管理員 : 3 : 26.0
超級管理員 : 2 : 23.5

 

 

源碼地址:https://github.com/caofanqi/study-spring-data-jpa

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM