1. 程式人生 > 實用技巧 >【Spring Data JPA】07 Specifications動態查詢

【Spring Data JPA】07 Specifications動態查詢

【前言說明】

針對CRUD種的查詢,因為我們的查詢總是具有各種各樣的篩選條件

為了我們的程式能夠更加適應篩選條件的變化,SpringDataJpa提供了Specifications這種解決方案

Specifications本意表示規範

也就是說我們的篩選條件也將需要被規範化

按照SpringDataJpa設計好的方式執行即可

【介面說明】

所在包位置:

org.springframework.data.jpa.repository.JpaSpecificationExecutor;

介面名稱:Java持久化介面規範處理器

所有抽象方法:

    Optional<T> findOne(@Nullable Specification<T> var1);

    List
<T> findAll(@Nullable Specification<T> var1); Page<T> findAll(@Nullable Specification<T> var1, Pageable var2); List<T> findAll(@Nullable Specification<T> var1, Sort var2); long count(@Nullable Specification<T> var1);

1、所有方法的Specification引數都被註解@Nullable,表示這個引數可以為Null,即表明可以無Specification條件來執行

2、findOne是表明查詢單個記錄,Specification即表明是一個篩選條件的物件

3、兩種查詢所有的findAll,其中一種必須要求排序引數,用於確定的排序需求使用

4、我們知道分頁必須要兩個SQL執行,所以這裡就有了Page & Long ,寫過分頁功能的就一定知道是組合使用的

檢視這個Specification,發現它也是一個介面

org.springframework.data.jpa.domain
public interface Specification<T> extends Serializable

在我們之前的學習中我們的篩選條件越來越多,我們不可能再為各個篩選條件編寫對應的引數

所以那個時候我們就需要統一起來各種篩選條件需要的引數就全部歸納為一個類,這就是篩選條件的引數類

但是不同的實體對映類,即我們的表的需要完成的功能不一樣,自然而然篩選的條件也不一樣

固定的一個條件引數類依然無法滿足更多ORM的需要,則進一步上升為一個條件引數規範

所以這就是Specification

而具體的條件細節則由我們自己來完成:

【雖然他已經設定好預設的一些東西了。。。】

    static <T> Specification<T> not(@Nullable Specification<T> spec) {
        return spec == null ? (root, query, builder) -> {
            return null;
        } : (root, query, builder) -> {
            return builder.not(spec.toPredicate(root, query, builder));
        };
    }

    @Nullable
    static <T> Specification<T> where(@Nullable Specification<T> spec) {
        return spec == null ? (root, query, builder) -> {
            return null;
        } : spec;
    }

    @Nullable
    default Specification<T> and(@Nullable Specification<T> other) {
        return SpecificationComposition.composed(this, other, (builder, left, rhs) -> {
            return builder.and(left, rhs);
        });
    }

    @Nullable
    default Specification<T> or(@Nullable Specification<T> other) {
        return SpecificationComposition.composed(this, other, (builder, left, rhs) -> {
            return builder.or(left, rhs);
        });
    }

    @Nullable
    Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);

對toPredicate的介紹:

要求引數:

Root 查詢的根物件(查詢的任何屬性從根物件獲取)
CriteriaQuery 頂層查詢物件,自定義查詢方式
CriteriaQueryBuilder 查詢構建器 封裝了很多查詢條件

單個記錄單個條件的查詢:

實現這個規範介面,並且重寫條件方法

從root物件獲取篩選條件的欄位【我需要根據什麼欄位來執行篩選條件?】

通過該Path物件被條件構建器注入和比較值進行比較

返回這個規範結果給我們的Dao方法使用

    @Test /* 查詢單個記錄  單個查詢條件 */
    public void findOne() {
        Specification<User> userSpecification = new Specification<User>(){

            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                // 獲取比較的屬性
                Path<Object> user_id = root.get("user_id");
                // 構建篩選條件 需要比較的欄位,欄位的值
                // Predicate predicate = criteriaBuilder.equal(user_id, 2);
                return criteriaBuilder.equal(user_id, 2); // 返回我們的比較結果?
            }
        };

        Optional<User> optionalUser = userRepository.findOne(userSpecification);
        User user = optionalUser.get();
        System.out.println(user);
    }

單個記錄多個條件的查詢:

使用構建器的and & or來合併條件

    @Test /* 查詢單個記錄 查詢多個條件 */
    public void findOnes() {

        /* Lambda表示式 */
        Specification<User> userSpecification = (Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
            // 獲取比較的屬性
            Path<Object> user_id = root.get("user_id");
            Path<Object> user_name = root.get("user_name");

            Predicate predicate01 = criteriaBuilder.equal(user_id, 2);
            Predicate predicate02 = criteriaBuilder.equal(user_name, "user01");

            // 如果多條件就合併條件
            // criteriaBuilder.and(predicate01, predicate02)
            // 或者不是全要求的條件,多條件其中一個滿足的情況
            // criteriaBuilder.or(predicate01, predicate02)
            // 具體情況根據實際需求來抉擇,或者組合

            return criteriaBuilder.and(predicate01, predicate02);
        };

        Optional<User> optionalUser = userRepository.findOne(userSpecification);
        User user = optionalUser.get();
        System.out.println(user);
    }

模糊條件的查詢:

    @Test /* 查詢多個記錄 查詢條件:模糊查詢 */
    public void findOneByLike() {

        /* Lambda表示式 */
        Specification<User> userSpecification = (Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
            // 獲取比較的屬性
            Path<Object> user_name = root.get("user_name");
            // 模糊要求指定引數型別
            return criteriaBuilder.like(user_name.as(String.class), "%user%");
        };

        List<User> userList = userRepository.findAll(userSpecification);
        for (User user : userList) {
            System.out.println(user);
        }
    }

多記錄排序條件查詢:

    @Test /* 查詢多個記錄 查詢條件:模糊查詢, 排序查詢 */
    public void findOneByLikeAndSort() {

        /* Lambda表示式 */
        Specification<User> userSpecification = (Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
            // 獲取比較的屬性
            Path<Object> user_name = root.get("userName");
            // 模糊要求指定引數型別
            return criteriaBuilder.like(user_name.as(String.class), "%use%");
        };

        // 引數?
        // Sort sortOrders = new Sort(Sort.Direction.DESC, "user_name");

        Sort sort = Sort.by(Sort.Direction.DESC, "userName");

        List<User> userList = userRepository.findAll(userSpecification, sort);

        for (User user : userList) {
            System.out.println(user);
        }
    }

排序條件查詢注入的屬性要求:

注意這裡排序使用的物件屬性!

因為這個錯誤導致我的實體類必須做出更改

package cn.echo42.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

/**
 * @author DaiZhiZhou
 * @file Spring-Data-JPA
 * @create 2020-07-31 22:56
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "sys_user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Integer userId; // @Column(name = "user_id")
    @Column(name = "user_name")
    private String userName; // @Column(name = "user_name")
    @Column(name = "user_password")
    private String userPassword; // @Column(name = "user_password")
    @Column(name = "user_status")
    private Integer userStatus; // @Column(name = "user_status")
    @Column(name = "user_is_del")
    private Integer userIsDel; // @Column(name = "user_is_del")
}

詳細原因是因為,排序的String引數properties,會將下劃線作為欄位的分隔符

我的user_name,就被分成user,name。這樣就無法匹配了啊。

解決方案是被迫更改實體類的屬性欄位為駝峰命名方式,並且註解上對應的表字段

參考地址:

https://zhidao.baidu.com/question/1823902339135786828.html

排序查詢的Sort條件物件的分析:

視訊參考地址:

https://www.bilibili.com/video/BV1WJ411j7TP?t=271&p=67

在老版本的SpringDataJPA中,原先的Sort允許被直接帶參構造的NEW出來

Sort sortOrders = new Sort(Sort.Direction.DESC, "user_name");

但是在現在的新版本中不再允許:

Sort的構造器不再允許被外部訪問

    private Sort(Sort.Direction direction, List<String> properties) {
        if (properties != null && !properties.isEmpty()) {
            this.orders = (List)properties.stream().map((it) -> {
                return new Sort.Order(direction, it);
            }).collect(Collectors.toList());
        } else {
            throw new IllegalArgumentException("You have to provide at least one property to sort by!");
        }
    }

    @Generated
    protected Sort(List<Sort.Order> orders) {
        this.orders = orders;
    }

第一種方式要求Direction物件和一個Properties的集合,

Direction是一個列舉類,意思是順序要求,無非就是ASC & DESC

Properties就是我們需要排序的欄位,一個或者是多個的存在

但是現在這個構造器不可使用了。。。

第二種方式要求一個Order物件的集合,SpringDataJPA的設計者希望由Order物件來封裝排序條件,

再裝入Sort處理,每一個Order都是對應的欄位和順序的要求。

這也反映出來,第一種的弊端就是隻能對所有的欄位同時ASC或者DESC,並不能分開要求

但是Sort類提供了靜態方法來實現物件的建立:

第一種提供properties屬性即可,要求的欄位均以預設的ASC排序

    public static Sort by(String... properties) {
        Assert.notNull(properties, "Properties must not be null!");
        return properties.length == 0 ? unsorted() : new Sort(DEFAULT_DIRECTION, Arrays.asList(properties));
    }

第二種提供Orders物件集合,對各個欄位的排序要求是獨立的

    public static Sort by(List<Sort.Order> orders) {
        Assert.notNull(orders, "Orders must not be null!");
        return orders.isEmpty() ? unsorted() : new Sort(orders);
    }

第三種就是由可變引數實現,和第二種區別不大

    public static Sort by(Sort.Order... orders) {
        Assert.notNull(orders, "Orders must not be null!");
        return new Sort(Arrays.asList(orders));
    }

第四種就是指定順序條件,第一種的補充

    public static Sort by(Sort.Direction direction, String... properties) {
        Assert.notNull(direction, "Direction must not be null!");
        Assert.notNull(properties, "Properties must not be null!");
        Assert.isTrue(properties.length > 0, "At least one property must be given!");
        return by((List)Arrays.stream(properties).map((it) -> {
            return new Sort.Order(direction, it);
        }).collect(Collectors.toList()));
    }

多記錄分頁條件查詢:

分頁條件SpringDataJPA要求一個Pageable型別的物件傳入

Pageable是一個介面,寓意可翻頁的

實現類有一個PageRequest

開啟PageRequest,同樣的,不允許呼叫構造器建立物件

    protected PageRequest(int page, int size, Sort sort) {
        super(page, size);
        Assert.notNull(sort, "Sort must not be null!");
        this.sort = sort;
    }

但是它由和Sort一樣提供了三種靜態方法:

    public static PageRequest of(int page, int size) {
        return of(page, size, Sort.unsorted());
    }

    public static PageRequest of(int page, int size, Sort sort) {
        return new PageRequest(page, size, sort);
    }

    public static PageRequest of(int page, int size, Direction direction, String... properties) {
        return of(page, size, Sort.by(direction, properties));
    }

第一個僅僅要求當前頁數和每頁顯示的記錄數量

第二個多了一個排序物件要求,即我們分頁之後再對這個結果集排序【盲猜】

第三個就是多欄位統一順序條件要求

對page條件的糾結:

注意這裡的page引數,以往我們的SQL的LIMIT查詢

是startPosition & sizeLimitation,即起始位置和記錄長度限制

分頁的頁數需要轉換成起始位置進行查詢

也就是這個:

(當前頁碼 - 1)*  每頁顯示數量 = 起始位置

在SpringDataJPA這裡已經幫我們處理好了,但是起始位置是從0開始

【需要處理一個再減一。。。】

演示案例:

    @Test /* 查詢多個記錄 查詢條件:分頁查詢 */
    public void findAllByPaging() {

        /* Lambda表示式 */
        Specification<User> userSpecification = (Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
            // 獲取比較的屬性
            Path<Object> user_name = root.get("userName");
            // 模糊要求指定引數型別
            return criteriaBuilder.like(user_name.as(String.class), "%use%");
        };

        Pageable pageable = PageRequest.of(1,2);
        
        Page<User> userPage = userRepository.findAll(userSpecification, pageable);
        List<User> userList = userPage.getContent();

        for (User user : userList) {
            System.out.println(user);
        }

        // 獲取總記錄數量 long totalElements = userPage.getTotalElements();
        // 獲取總頁數 int totalPages = userPage.getTotalPages();
    }