REST-Abfragesprache - OR-Operation implementieren

REST-Abfragesprache - Implementierung oder Operation

1. Überblick

In diesem kurzen Artikel erweitern wir die erweiterten Suchvorgänge, die wir inprevious article implementiert haben, und schließenOR-based search criteria into our REST API Query Language ein.

2. Implementierungsansatz

Zuvor wurden alle Kriterien im Abfrageparametersearchals Prädikate gebildet, die nur nach dem AND-Operator gruppiert waren. Lassen Sie uns das ändern.

Wir sollten in der Lage sein, diese Funktion entweder als einfache, schnelle Änderung eines bestehenden oder eines neuen Ansatzes von Grund auf zu implementieren.

Mit dem einfachen Ansatz kennzeichnen wir die Kriterien, um anzuzeigen, dass sie mit dem Operator OR kombiniert werden müssen.

Hier ist beispielsweise die URL zum Testen der API auf „firstName OR lastName”:

http://localhost:8080/users?search=firstName:john,'lastName:doe

Beachten Sie, dass wir die KriterienlastName mit einem einfachen Anführungszeichen gekennzeichnet haben, um sie zu unterscheiden. Wir werden dieses Prädikat für den OR-Operator in unserem Kriterienwertobjekt -SpecSearchCriteria: erfassen

public SpecSearchCriteria(
  String orPredicate, String key, SearchOperation operation, Object value) {
    super();

    this.orPredicate
      = orPredicate != null
      && orPredicate.equals(SearchOperation.OR_PREDICATE_FLAG);

    this.key = key;
    this.operation = operation;
    this.value = value;
}

3. UserSpecificationBuilder Verbesserung

Ändern wir nun unseren SpezifikationsgeneratorUserSpecificationBuilder,, um die OR-qualifizierten Kriterien bei der Erstellung vonSpecification<User> zu berücksichtigen:

public Specification build() {
    if (params.size() == 0) {
        return null;
    }
    Specification result = new UserSpecification(params.get(0));

    for (int i = 1; i < params.size(); i++) {
        result = params.get(i).isOrPredicate()
          ? Specification.where(result).or(new UserSpecification(params.get(i)))
          : Specification.where(result).and(new UserSpecification(params.get(i)));
    }
    return result;
 }

4. UserController Verbesserung

Lassen Sie uns abschließend einen neuen REST-Endpunkt in unserem Controller einrichten, um diese Suchfunktion mit dem Operator OR zu verwenden. Die verbesserte Parsing-Logik extrahiert das spezielle Flag, mit dessen Hilfe die Kriterien mit dem Operator OR identifiziert werden können:

@GetMapping("/users/espec")
@ResponseBody
public List findAllByOrPredicate(@RequestParam String search) {
    Specification spec = resolveSpecification(search);
    return dao.findAll(spec);
}

protected Specification resolveSpecification(String searchParameters) {
    UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
    String operationSetExper = Joiner.on("|")
      .join(SearchOperation.SIMPLE_OPERATION_SET);
    Pattern pattern = Pattern.compile(
      "(\\p{Punct}?)(\\w+?)("
      + operationSetExper
      + ")(\\p{Punct}?)(\\w+?)(\\p{Punct}?),");
    Matcher matcher = pattern.matcher(searchParameters + ",");
    while (matcher.find()) {
        builder.with(matcher.group(1), matcher.group(2), matcher.group(3),
        matcher.group(5), matcher.group(4), matcher.group(6));
    }

    return builder.build();
}

5. Live-Test mitOR Bedingung

In diesem Live-Testbeispiel suchen wir mit dem neuen API-Endpunkt nach Benutzern mit dem Vornamen "john" ODER dem Nachnamen "doe". Beachten Sie, dass der ParameterlastName ein einfaches Anführungszeichen hat, das ihn als "ODER-Prädikat" qualifiziert:

private String EURL_PREFIX
  = "http://localhost:8082/spring-rest-full/auth/users/espec?search=";

@Test
public void givenFirstOrLastName_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(EURL_PREFIX + "firstName:john,'lastName:doe");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertTrue(result.contains(userTom.getEmail()));
}

6. Persistenztest mitOR Bedingung

Lassen Sie uns nun denselben Test wie oben auf der Persistenzstufe für Benutzerwith first name “john” OR last name “doe”durchführen:

@Test
public void givenFirstOrLastName_whenGettingListOfUsers_thenCorrect() {
    UserSpecificationsBuilder builder = new UserSpecificationsBuilder();

    SpecSearchCriteria spec
      = new SpecSearchCriteria("firstName", SearchOperation.EQUALITY, "john");
    SpecSearchCriteria spec1
      = new SpecSearchCriteria("'","lastName", SearchOperation.EQUALITY, "doe");

    List results = repository
      .findAll(builder.with(spec).with(spec1).build());

    assertThat(results, hasSize(2));
    assertThat(userJohn, isIn(results));
    assertThat(userTom, isIn(results));
}

7. Alternativer Ansatz

Im alternativen Ansatz könnten wir die Suchabfrage eher wie eine vollständigeWHERE-Klausel der SQL-Abfrage bereitstellen.

Hier ist beispielsweise die URL für eine komplexere Suche nachfirstName undage:

http://localhost:8080/users?search=( firstName:john OR firstName:tom ) AND age>22

Beachten Sie, dass wir einzelne Kriterien, Operatoren und Gruppierungsklammern durch ein Leerzeichen getrennt haben, um einen gültigen Infix-Ausdruck zu bilden.

Analysieren wir den Infix-Ausdruck mitCriteriaParser. UnserCriteriaParser teilt den angegebenen Infix-Ausdruck in Token (Kriterien, Klammern, AND & OR-Operatoren) auf und erstellt einen Postfix-Ausdruck für denselben:

public Deque parse(String searchParam) {

    Deque output = new LinkedList<>();
    Deque stack = new LinkedList<>();

    Arrays.stream(searchParam.split("\\s+")).forEach(token -> {
        if (ops.containsKey(token)) {
            while (!stack.isEmpty() && isHigerPrecedenceOperator(token, stack.peek())) {
                output.push(stack.pop().equalsIgnoreCase(SearchOperation.OR_OPERATOR)
                  ? SearchOperation.OR_OPERATOR : SearchOperation.AND_OPERATOR);
            }
            stack.push(token.equalsIgnoreCase(SearchOperation.OR_OPERATOR)
              ? SearchOperation.OR_OPERATOR : SearchOperation.AND_OPERATOR);

        } else if (token.equals(SearchOperation.LEFT_PARANTHESIS)) {
            stack.push(SearchOperation.LEFT_PARANTHESIS);
        } else if (token.equals(SearchOperation.RIGHT_PARANTHESIS)) {
            while (!stack.peek().equals(SearchOperation.LEFT_PARANTHESIS)) {
                output.push(stack.pop());
            }
            stack.pop();
        } else {
            Matcher matcher = SpecCriteraRegex.matcher(token);
            while (matcher.find()) {
                output.push(new SpecSearchCriteria(
                  matcher.group(1),
                  matcher.group(2),
                  matcher.group(3),
                  matcher.group(4),
                  matcher.group(5)));
            }
        }
    });

    while (!stack.isEmpty()) {
        output.push(stack.pop());
    }

    return output;
}


Fügen wir in unserem Spezifikations-BuilderGenericSpecificationBuilder, eine neue Methode hinzu, um die SucheSpecification aus dem Postfix-Ausdruck zu erstellen:

    public Specification build(Deque postFixedExprStack,
        Function> converter) {

        Deque> specStack = new LinkedList<>();

        while (!postFixedExprStack.isEmpty()) {
            Object mayBeOperand = postFixedExprStack.pollLast();

            if (!(mayBeOperand instanceof String)) {
                specStack.push(converter.apply((SpecSearchCriteria) mayBeOperand));
            } else {
                Specification operand1 = specStack.pop();
                Specification operand2 = specStack.pop();
                if (mayBeOperand.equals(SearchOperation.AND_OPERATOR)) {
                    specStack.push(Specification.where(operand1)
                      .and(operand2));
                }
                else if (mayBeOperand.equals(SearchOperation.OR_OPERATOR)) {
                    specStack.push(Specification.where(operand1)
                      .or(operand2));
                }
            }
        }
        return specStack.pop();

Fügen wir abschließend einen weiteren REST-Endpunkt in unsereUserController ein, um den komplexen Ausdruck mit den neuenCriteriaParser zu analysieren:

@GetMapping("/users/spec/adv")
@ResponseBody
public List findAllByAdvPredicate(@RequestParam String search) {
    Specification spec = resolveSpecificationFromInfixExpr(search);
    return dao.findAll(spec);
}

protected Specification resolveSpecificationFromInfixExpr(String searchParameters) {
    CriteriaParser parser = new CriteriaParser();
    GenericSpecificationsBuilder specBuilder = new GenericSpecificationsBuilder<>();
    return specBuilder.build(parser.parse(searchParameters), UserSpecification::new);
}

8. Fazit

In diesem Tutorial haben wir unsere REST-Abfragesprache um die Möglichkeit erweitert, mit einem ODER-Operator zu suchen.

Die vollständige Implementierung dieses Artikels finden Sie inthe GitHub project. Dies ist ein Maven-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein.