首頁 > 軟體

SpringBoot+Querydsl 框架實現複雜查詢解析

2022-05-31 18:00:56

概述

查詢功能是在各種應用程式裡面都有應用,且非常重要的功能。使用者直接使用的查詢功能往往是在我們做好的UI介面上進行查詢,UI會將查詢請求發給查詢實現的伺服器,或者專門負責實現查詢的一個元件。市場上有專門做查詢的框架,其中比較出名,應用也比較廣泛的是elasticsearch。

定義查詢請求

對於伺服器端來說,前端UI傳送過來的查詢請求必然是按一定規則組織起來的,這樣的規則後端必須能夠支援和解析。換一種說法就是服務呼叫者和服務釋出者之間需要遵循同一個規範才可以。百度的UI查詢是這樣定義的:

在上圖中加了藍色下劃線的地方即為我們在百度當中搜尋的字串內容,可以發現,百度的實現是將搜尋的內容當做了http請求的url的引數來處理的,用了一個q作為key,q後面的內容就是所查詢的內容。

google的實現是類似的,如下圖所示:

對於google和百度這樣的只有一個搜尋方塊的查詢介面,這樣處理是比較合理的,也不是整個查詢實現最關鍵的部分。更為關鍵的是後續伺服器將這個查詢內容進行了怎樣的處理。對於別的一些產品來說,可能需要對某些關鍵字進行單獨的查詢,這個時候肯定就不是一個搜尋方塊能個滿足的需求了。

總的來說,我們可以有如下的方式來組織一個查詢

google-like查詢

這種查詢典型的應用是一個查詢框,什麼都可以查的情形,例如google和百度。對於這樣的查詢需求來說,在構建查詢請求時只需將查詢的內容放在http請求的的引數裡面即可。

這樣的查詢解析是非常方便的,難度和需要考慮得事情在於要講查詢的內容放到哪些地方去查詢。從資料庫的層面來說就是要去哪些資料庫的哪些表去查詢。

特定欄位的類sql查詢

這種查詢是指定某個欄位,然後採用類似於sql語句的寫法進行查詢,各種查詢條件以一定的形式組織在一起,發給伺服器進行解析。這樣的查詢對伺服器解析查詢的能力要求更高,它提供了一些更加具體的查詢條件。

例如我們以冒號表示等於,則一個查詢字串的形式是:

name:bill

這個查詢的意思就是查詢名字name等於bill的記錄。

我們也可以將多個條件拼接在一起,讓他們直接用邏輯關係組合在一起,例如或者和並且的邏輯關係。例如:

name:bill AND city:LA

或者下面這種或者的關係:

name:bill OR city:LA

上面的查詢語句意味著我們的前後臺要定義一套自己的查詢邏輯和架構,並且解析它,並將它轉換為正確的查詢。若我們想實現靈活的查詢,則上面的查詢語句在符合規則的前提下應當是可以自由組合的。怎麼做取決於我們的實際需求。如果一個寫死的查詢關鍵字就能滿足我們的需求,則在當前那個時期自然也是合理的。

但是從靈活性角度,技術角度,實現成靈活的可解析的,顯然是我們更想要的功能。最靈活的當然就是sql語句能支援怎樣的查詢,我們都能支援對應的查詢寫法,但是這對伺服器的解析邏輯就有了更加高的要求,尤其是當主表子表混在一起查詢之後,會更加複雜

使用Spring Data Querydsl

什麼是Querydsl呢?Querydsl是一個框架,它可以通過它提供的的API幫助我們構建靜態型別的SQL-like查詢,也就是在上面我們提到的組織查詢方式。可以通過諸如Querydsl之類的流暢API構造查詢。

Querydsl是出於以型別安全的方式維護HQL查詢的需要而誕生的。 HQL查詢的增量構造需要String連線,這導致難以閱讀的程式碼。通過純字串對域型別和屬性的不安全參照是基於字串的HQL構造的另一個問題。

隨著域模型的不斷變化,型別安全性在軟體開發中帶來了巨大的好處。域更改直接反映在查詢中,而查詢構造中的自動完成功能使查詢構造更快,更安全。

用於Hibernate的HQL是Querydsl的第一個目標語言,如今querydsl支援JPA,JDO,JDBC,Lucene,Hibernate Search,MongoDB,Collections和RDFBean作為它的後端。

其官方網站在這裡:http://querydsl.com/

Querydsl和spring有什麼關係呢?幾個Spring Data的模組通過QuerydslPredicateExecutor提供了與Querydsl的整合,如以下範例所示:

public interface QuerydslPredicateExecutor<T> {
//查詢並返回與Predicate匹配的單個entity。
  Optional<T> findById(Predicate predicate);  
//查詢並返回與Predicate匹配的所有entity
  Iterable<T> findAll(Predicate predicate);   
//返回與Predicate匹配的數量。
  long count(Predicate predicate);            
//返回是否存在與Predicate匹配的entity。
  boolean exists(Predicate predicate);        
 
  // … more functionality omitted.
}

Predicate就是我們需要傳入的一個查詢的抽象。

在spring當中使用Querydsl,只需要在spring的repository介面繼承QuerydslPredicateExecutor,如以下範例所示:

interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

在定義了上面的這個介面之後,我們就可以使用Querydsl Predicate編寫type-safe的查詢,如以下範例所示:

Predicate predicate = user.firstname.equals("dave")
 .and(user.lastname.startsWith("mathews"));
 
userRepository.findAll(predicate);

上面的程式碼構建出的predicate體現在sql語句裡的話就是這樣的: where firstname = 'dave' and lastname ='mathews%'。這就是所謂的類sql的查詢,用起來非常的直觀。

因此,我們可以將我們接收到的查詢請求,轉化為對應的predicte,且從技術上講,只要predict支援的查詢拼接我們都能支援,難點只在於如何解析查詢請求,以及如何將他們轉換為對應的predicate.

利用Spring Query DSL實現動態查詢

下面是使用spring和Querydsl實現動態查詢的一個例子.

現在假設我們有Model類如下:

public class Student {
    private String id;
    private String gender;
    private String firstName;
    private String lastName;
    private Date createdAt;
    private Boolean isGraduated;
}

我們希望可以實現該類所有欄位直接自由組合進行查詢,且可以按照與和或的邏輯進行查詢。且我們約定用冒號表示等於,例如:

firstname:li AND lastname:hua
firstname:li OR lastname:hua
firstname:li AND lastname:hua AND gender:male

上面的查詢都比較清晰,解析不會有太大難度,下面我們來看這樣一個查詢:

firstname:li OR lastname:hua AND gender:male

這個查詢的問題在於作為邏輯與的gender查詢,到底是隻和前面一個條件進行與操作,還是與前面兩個條件一起進行一個與操作,顯然與的條件往往是作為filter的功能出現的。

因此我們應當將其看作整個其他條件的與操作,因此我們需要先將前面的條在組合在一起,例如,我們可以使用括號表示這個邏輯,那麼查詢就會變成:

(firstname:li AND lastname:hua) AND gender:male

這下邏輯就變得清晰了,難題就在於怎麼解析了

public class QueryAnalysis{
    private static final String EMPTY_STRING = "";
    private static final String BLANK_STRING = " ";
    private static final String COLON = ":";
    private static final String BP_CATEGORY_CODE = "categoryCode";
    private static final String OPEN_PARENTTHESIS = "(";
    private static final String CLOSE_PARENTTHESIS = ")";
    private static final String QUERY_REGEX = "([\w.]+?)(:|<|>|!:)([^ ]*)";
    //it has to lie between two blanks
    private static final String QUERY_LOGIC_AND = " AND ";
    private void generateQueryBuilderWithQueryString(PredicateBuilder builder, String q,
            List<String> queryStringList) {
        StringBuilder stringBuilder = new StringBuilder();
        String queryTerm = q;
        if (q == null) {
            return;
        }
        if (!q.contains(" AND ") && !q.startsWith("(") && !q.endsWith(")")) {
            queryTerm = stringBuilder.append("(").append(q).append(")").toString();
        }
        Map<String, Matcher> matcherMap = getMatcherWithQueryStr(queryTerm);
        Matcher matcherOr = matcherMap.get("matcherOr");
        Matcher matcherAnd = matcherMap.get("matcherAnd");
        while (matcherOr.find()) {
            builder.withOr(matcherOr.group(1), matcherOr.group(2), matcherOr.group(3));
        }
        while (matcherAnd.find()) {
            builder.withAnd(matcherAnd.group(1), matcherAnd.group(2), matcherAnd.group(3));
            isSearchParameterValid = true;
        }
   }
    private static Map<String, Matcher> getMatcherWithQueryStr(String q) {
        StringBuilder stringBuilder = new StringBuilder();
        Pattern pattern = Pattern.compile(QUERY_REGEX);
        // inside the subString is "or",outside them are "and"
        String[] queryStringArraySplitByAnd = q.split(QUERY_LOGIC_AND);
        String queryStringOr = EMPTY_STRING;
        String queryStringAnd = EMPTY_STRING;
        for (String string : queryStringArraySplitByAnd) {
            if (string.trim().startsWith(OPEN_PARENTTHESIS) && string.trim().endsWith(CLOSE_PARENTTHESIS)) {
                //only support one OR sentence
                queryStringOr = string.trim().substring(1,string.length()-1);
            } else {
                queryStringAnd = stringBuilder.append(string).append(BLANK_STRING).toString();
            }
        }
        String queryStringAndTrim = queryStringAnd.trim();
        if(queryStringAndTrim.startsWith(OPEN_PARENTTHESIS) && queryStringAndTrim.endsWith(CLOSE_PARENTTHESIS)){
            queryStringAnd = queryStringAndTrim.substring(1,queryStringAndTrim.length()-1);
        }
        Matcher matcherOr = pattern.matcher(queryStringOr);
        Matcher matcherAnd = pattern.matcher(queryStringAnd);
        Map<String, Matcher> matcherMap = new ConcurrentHashMap<>();
        matcherMap.put("matcherOr", matcherOr);
        matcherMap.put("matcherAnd", matcherAnd);
        return matcherMap;
    }
}

Predicate的邏輯如下:

import java.util.ArrayList;
import java.util.List;
 
import com.querydsl.core.types.dsl.BooleanExpression;
 
/**
 * This class is mainly used to classify all the query parameters
 */
public class PredicateBuilder {
 
    private static final String BLANK_STRING = " ";
 
    private static final String TILDE_STRING = "~~";
 
    private List<SearchCriteria> paramsOr;
 
    private List<SearchCriteria> paramsAnd;
 
    private BusinessPartnerMessageProvider messageProvider;
    
    public PredicateBuilder(BusinessPartnerMessageProvider messageProvider){
        paramsOr = new ArrayList<>();
        paramsAnd = new ArrayList<>();
    }
 
    public PredicateBuilder withOr(
            String key, String operation, Object value) {
        String keyAfterConverted = keyConverter(key);
        Object valueAfterConverted = value.toString().replaceAll(TILDE_STRING,BLANK_STRING).trim();
        paramsOr.add(new SearchCriteria(keyAfterConverted, operation, valueAfterConverted));
        return this;
    }
 
    public PredicateBuilder withAnd(
            String key, String operation, Object value) {
        String keyAfterConverted = keyConverter(key);
        Object valueAfterConverted = value.toString().replaceAll(TILDE_STRING,BLANK_STRING).trim();
        paramsAnd.add(new SearchCriteria(keyAfterConverted, operation, valueAfterConverted));
        return this;
    }
 
    protected String keyConverter(String key){
        return key;
    }
 
    public BooleanExpression buildOr(Class classType) {
        
        return handleBPBooleanExpressionOr(classType);
    }
 
    public BooleanExpression buildAnd(Class classType) {
 
        return handleBPBooleanExpressionAnd(classType);
    }
 
 
    private BooleanExpression handleBPBooleanExpressionOr(Class classType) {
 
        if (paramsOr.isEmpty()) {
            return null;
        }
        return buildBooleanExpressionOr(paramsOr, classType);
 
    }
 
    private BooleanExpression handleBPBooleanExpressionAnd(Class classType) {
        if (paramsAnd.isEmpty()) {
            return null;
        }
        return buildBooleanExpressionAnd(paramsAnd, classType);
 
    }
 
    private BooleanExpression buildBooleanExpressionOr(List<SearchCriteria> paramsOr, Class classType){
        List<BooleanExpression> predicates = new ArrayList<>();
        BooleanExpressionBuilder predicate;
        for (SearchCriteria param : paramsOr) {
 
            predicate = new BooleanExpressionBuilder(param, messageProvider);
 
            BooleanExpression exp = predicate.buildPredicate(classType);
 
            if (exp != null) {
                predicates.add(exp);
            }
        }
        BooleanExpression result = null;
        if(!predicates.isEmpty()) {
            result = predicates.get(0);
            for (int i = 1; i < predicates.size(); i++) {
                result = result.or(predicates.get(i));
            }
        }
        return result;
    }
 
    private BooleanExpression buildBooleanExpressionAnd(List<SearchCriteria> paramsAnd, Class classType){
        List<BooleanExpression> predicates = new ArrayList<>();
        BooleanExpressionBuilder predicate;
        for (SearchCriteria param : paramsAnd) {
 
            predicate = new BooleanExpressionBuilder(param, messageProvider);
 
            BooleanExpression exp = predicate.buildPredicate(classType);
 
            if (exp != null) {
                predicates.add(exp);
            }
        }
        BooleanExpression result = null;
        if(!predicates.isEmpty()) {
            result = predicates.get(0);
            for (int i = 1; i < predicates.size(); i++) {
                result = result.and(predicates.get(i));
            }
        }
        return result;
    }
 
}

BooleanExpressionBuilder的邏輯如下:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.TimeZone;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.BooleanPath;
import com.querydsl.core.types.dsl.DateTimePath;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.core.types.dsl.StringPath;
public class BooleanExpressionBuilder {
    private SearchCriteria criteria;
    private BusinessPartnerMessageProvider messageProvider;
    private static final String NO_SUCH_FILED_MESSAGE = "NO_SUCH_FIELD_FOR_QUERY_PARAMETER";
    public BooleanExpressionBuilder(final SearchCriteria criteria ) {
        this.criteria = new SearchCriteria(criteria.getKey(),criteria.getOperation(),criteria.getValue());
    }
    public BooleanExpression buildPredicate(Class classType) {
        // the second param for PathBuilder constructor is the binding path.
        PathBuilder<Class> entityPath = new PathBuilder<>(classType, classType.getSimpleName());
        Boolean isValueMatchEndWith = criteria.getValue().toString().endsWith("*");
        Boolean isValueMatchStartWith = criteria.getValue().toString().startsWith("*");
        Boolean isOperationColon = ":".equalsIgnoreCase(criteria.getOperation());
        int searchValueLength = criteria.getValue().toString().length();
        StringPath stringPath = entityPath.getString(criteria.getKey());
        DateTimePath<Date> timePath = entityPath.getDateTime(criteria.getKey(), Date.class);
        NumberPath<Integer> numberPath = entityPath.getNumber(criteria.getKey(), Integer.class);
        if ((isOperationColon) && (!isValueMatchStartWith) && (!isValueMatchEndWith)) {
            return getEqualBooleanExpression(classType, entityPath, stringPath, timePath, numberPath);
        }
        if (">".equalsIgnoreCase(criteria.getOperation())) {
            return getGreaterThanBooleanExpression(classType, timePath, numberPath);
        }
        if ("<".equalsIgnoreCase(criteria.getOperation())) {
            return getLessThanBooleanExpression(classType, timePath, numberPath);
        }
        // !:means !=
        if ("!:".equalsIgnoreCase(criteria.getOperation())) {
            return getNotEqualBooleanExpression(classType, entityPath,
                    stringPath, timePath, numberPath);
        }
        //start with xxx
        if ((isOperationColon) && isValueMatchEndWith && (!isValueMatchStartWith)) {
            if (isSearchKeyValidForClass(classType))
                return stringPath
                        .startsWithIgnoreCase(criteria.getValue().toString().substring(0, searchValueLength - 1).trim());
        }
        if ((isOperationColon) && (!isValueMatchEndWith) && (isValueMatchStartWith)) {
            if (isSearchKeyValidForClass(classType))
                return stringPath.endsWithIgnoreCase(criteria.getValue().toString().substring(1, searchValueLength).trim());
        }
        //contain xxx
        if ((isOperationColon) && isValueMatchEndWith && isValueMatchStartWith) {
            return getContainsBooleanExpression(classType, searchValueLength, stringPath);
        }
        return null;
    }
    private BooleanExpression getContainsBooleanExpression(Class classType,
            int searchValueLength, StringPath stringPath) {
            try {
                Class fieldType = classType.getDeclaredField(criteria.getKey()).getType();
                if (fieldType.equals(String.class) && searchValueLength>1) {
                    return stringPath.containsIgnoreCase(criteria.getValue().toString().substring(1,searchValueLength-1).trim());
                }
                //if there are only a "*" in the seatch value, then
                if(fieldType.equals(String.class) && searchValueLength==1){
                    return stringPath.eq(criteria.getValue().toString());
                }
            } catch (NoSuchFieldException | SecurityException e) {
            }
        return null;
    }
    private boolean isSearchKeyValidForClass(Class classType) {
        try {
            Class fieldType = classType.getDeclaredField(criteria.getKey()).getType();
            if (fieldType.equals(String.class)) {
                return true;
            }
        } catch (NoSuchFieldException | SecurityException e) {
            throw new BadRequestValidationException(messageProvider.getMessage(NO_SUCH_FILED_MESSAGE,
                    new Object[] { criteria.getKey() }), e);
        }
        return false;
    }
    private BooleanExpression getNotEqualBooleanExpression(Class classType, PathBuilder<Class> entityPath,
            StringPath stringPath, DateTimePath<Date> timePath, NumberPath<Integer> numberPath) {
            try {
                Class fieldType = classType.getDeclaredField(criteria.getKey()).getType();
                if (fieldType.equals(Date.class)) {
                    dateTimeValueConverter();
                    return timePath.ne((Date) criteria.getValue());
                }
                if (fieldType.equals(Integer.class)) {
                    int value = Integer.parseInt(criteria.getValue().toString());
                    return numberPath.ne(value);
                }
                if (fieldType.equals(String.class)) {
                    return stringPath.ne(criteria.getValue().toString());
                }
                if (fieldType.equals(boolean.class)) {
                    booleanConverter();
                    BooleanPath booleanPath = entityPath.getBoolean(criteria.getKey());
                    return booleanPath.ne((Boolean) criteria.getValue());
                }
                if (fieldType.equals(Boolean.class)) {
                    booleanConverter();
                    BooleanPath booleanPath = entityPath.getBoolean(criteria.getKey());
                    return booleanPath.ne((Boolean) criteria.getValue());
                }
            } catch (NoSuchFieldException | SecurityException e) {
                throw new BadRequestValidationException();
            }
        return null;
    }
    private BooleanExpression getLessThanBooleanExpression(Class classType,
            DateTimePath<Date> timePath, NumberPath<Integer> numberPath) {
            try {
                Class fieldType = classType.getDeclaredField(criteria.getKey()).getType();
                if (fieldType.equals(Date.class)) {
                    dateTimeValueConverter();
                    return timePath.lt((Date) criteria.getValue());
                }
                if (fieldType.equals(Integer.class)) {
                    integerValueConverter();
                    return numberPath.lt((Integer) criteria.getValue());
                }
            } catch (NoSuchFieldException | SecurityException e) {
                throw new BadRequestValidationException(e.getCause());
            }
        return null;
    }
    private BooleanExpression getGreaterThanBooleanExpression(Class classType,
            DateTimePath<Date> timePath, NumberPath<Integer> numberPath) {
            // other data types do not make sense when use >
            try {
                Class fieldType = classType.getDeclaredField(criteria.getKey()).getType();
                if (fieldType.equals(Date.class)) {
                    dateTimeValueConverter();
                    return timePath.gt((Date) criteria.getValue());
                }
                if (fieldType.equals(Integer.class)) {
                    integerValueConverter();
                    return numberPath.gt((Integer) criteria.getValue());
                }
            } catch (NoSuchFieldException | SecurityException e) {
                throw new BadRequestValidationException(e.getCause());
            }
        return null;
    }
    private BooleanExpression getEqualBooleanExpression(Class classType, PathBuilder<Class> entityPath, StringPath stringPath,
            DateTimePath<Date> timePath, NumberPath<Integer> numberPath) {
        // :means =
            try {
                Class fieldType = classType.getDeclaredField(criteria.getKey()).getType();
                if (fieldType.equals(Integer.class)) {
                    integerValueConverter();
                    return numberPath.eq((Integer) criteria.getValue());
                }
                if (fieldType.equals(Date.class)) {
                    dateTimeValueConverter();
                    return timePath.eq((Date) criteria.getValue());
                }
                if (fieldType.equals(boolean.class)) {
                    booleanConverter();
                    BooleanPath booleanPath = entityPath.getBoolean(criteria.getKey());
                    return booleanPath.eq((Boolean) criteria.getValue());
                }
                if (fieldType.equals(Boolean.class)) {
                    booleanConverter();
                    BooleanPath booleanPath = entityPath.getBoolean(criteria.getKey());
                    return booleanPath.eq((Boolean) criteria.getValue());
                }
                if (fieldType.equals(String.class)) {
                    return stringPath.equalsIgnoreCase(criteria.getValue().toString());
                }
            } catch (NoSuchFieldException | SecurityException e) {
                throw new BadRequestValidationException(e.getCause());
            }
        return null;
    }
    // convert string to datetime
    private void dateTimeValueConverter() {
        criteria.setValue(convertToTimeStamp(criteria.getValue().toString()));
    }
    private void  booleanConverter() {
        if (criteria.getValue().toString().equalsIgnoreCase("true")) {
            criteria.setValue(true);
        } else if (criteria.getValue().toString().equalsIgnoreCase("false")) {
            criteria.setValue(false);
        } else {
            throw new BadRequestValidationException("Invalid Boolean");
        }
    }
    // convert string to Integer
    private void integerValueConverter() {
        criteria.setValue(Integer.parseInt(criteria.getValue().toString()));
    }
    private Date convertToTimeStamp(String time) {
        //convert date here
        return parsedDate;
    }
}

查詢條件的抽象類SearchCriteria定義如下:

public class SearchCriteria {
    private String key;
    private String operation;
    private Object value;
}

大致的實現邏輯如下圖所示:

比較關鍵的點有下面這些:

  • 對字串的解析需要藉助正規表示式的幫助,正規表示式決定了我們支援怎樣的查詢.
  • 由於字串可以任意輸入,存在無限種可能,對查詢字串的校驗很關鍵也很複雜。
  • 不同邏輯的查詢條件需要存放在不同的容器裡面,因為他們的拼接邏輯不一樣,一個是或一個是與
  • 不同的欄位型別需要呼叫不同的生成Predicate的方法,例如String,Boolean和Date這些型別他們都有自己對應的查詢實現
  • 生成子表的Predicate很複雜,與主表的查詢條件一起查詢時邏輯更加複雜,上面的邏輯拿掉了這一部分。但是這個功能是可以實現的。
  • 實現過程中的難題 主表包含多個子表資料時的AND查詢

距離說明,現在有資料定義如下:

{
 "customerNumber": "5135116903",
 "customerType": "INDIVIDUAL",
 "createdBy": "Android.chen@sap.com",
 "changedBy": "Android.chen@sap.com",
 "createdAt": "2018-06-26T10:15:17.212Z",
 "changedAt": "2018-06-26T10:15:17.212Z",
 "markets": [{
  "marketId": "A1",
  "currency": "USD",
  "country": "US",
  "active": true
 }, {
  "marketId": "A2",
  "currency": "USD",
  "country": "US",
  "active": false
 }, {
  "marketId": "A3",
  "currency": "USD",
  "country": "US",
  "active": true
 }]
}

其中父節點表是customer,子節點markets資訊儲存在market表當中。

現在,假設我們有這樣的查詢:

customerNumber: 5135116903 AND markets.active:false

沒有疑問,上面的資料應該被查出來。現在查詢條件變成下面這樣:

customerNumber: 5135116903 AND markets.active:false AND markets.marketId:A1

現在問題來了,語句的意思是此客戶的marker既要是非active 的且ID要是A1,但是此客戶又有多個market,從整個陣列裡來看,這個條件是滿足的。但是從單個的market個體來看這個條件是不滿足的。而我們作為使用者的話希望得到的效果必然是無法查處此customer資訊。

這會給實現帶來問題,因為由於market是一個陣列,在資料表中對應的就是幾條記錄,我們在解析並構建子表查詢時,必須確保對於子表的查詢條件是作用於單獨的一個node,也就是單獨的一條記錄,而不是從整個陣列當中去查,否則就會有問題。

到此這篇關於SpringBoot+Querydsl 框架實現複雜查詢解析的文章就介紹到這了,更多相關SpringBoot Querydsl 查詢內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com