REST-Abfragesprache mit RSQL

REST Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> PRÜFEN SIE DEN KURS Persistenz oben

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> PRÜFEN SIE DEN KURS Dieser Artikel ist Teil einer Reihe: • REST-Abfragesprache mit Spring- und JPA-Kriterien

• REST-Abfragesprache mit Spring Data JPA-Spezifikationen

• REST-Abfragesprache mit Spring Data JPA und Querydsl

• REST-Abfragesprache - Erweiterte Suchvorgänge

• REST-Abfragesprache - Implementierung einer ODER-Operation

• REST-Abfragesprache mit RSQL (aktueller Artikel) • REST-Abfragesprache mit Querydsl-Webunterstützung

1. Übersicht

In diesem fünften Artikel der Reihe werden wir das Erstellen der REST-API-Abfragesprache mithilfe einer coolen Bibliothek - dem rsql-Parser - veranschaulichen.

RSQL ist ein Super-Set der Feed Item Query Language (FIQL) - eine saubere und einfache Filtersyntax für Feeds. es passt also ganz natürlich in eine REST-API.

2. Vorbereitungen

Fügen wir zunächst der Bibliothek eine Maven-Abhängigkeit hinzu:

 cz.jirutka.rsql rsql-parser 2.1.0 

Und auch die Haupt-Einheit definieren wir mit allen Beispielen gehen zu arbeiten - Benutzer :

@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; }

3. Analysieren Sie die Anfrage

Die interne Darstellung von RSQL-Ausdrücken erfolgt in Form von Knoten, und das Besuchermuster wird zum Analysieren der Eingabe verwendet.

In diesem Sinne werden wir die RSQLVisitor- Schnittstelle implementieren und unsere eigene Besucherimplementierung erstellen - CustomRsqlVisitor :

public class CustomRsqlVisitor implements RSQLVisitor
     
       { private GenericRsqlSpecBuilder builder; public CustomRsqlVisitor() { builder = new GenericRsqlSpecBuilder(); } @Override public Specification visit(AndNode node, Void param) { return builder.createSpecification(node); } @Override public Specification visit(OrNode node, Void param) { return builder.createSpecification(node); } @Override public Specification visit(ComparisonNode node, Void params) { return builder.createSecification(node); } }
     

Jetzt müssen wir uns mit der Persistenz befassen und unsere Abfrage aus jedem dieser Knoten erstellen.

Wir werden die Spring Data JPA-Spezifikationen verwenden, die wir zuvor verwendet haben - und wir werden einen Specification Builder implementieren , um Spezifikationen aus jedem dieser Knoten zu erstellen, die wir besuchen :

public class GenericRsqlSpecBuilder { public Specification createSpecification(Node node) { if (node instanceof LogicalNode) { return createSpecification((LogicalNode) node); } if (node instanceof ComparisonNode) { return createSpecification((ComparisonNode) node); } return null; } public Specification createSpecification(LogicalNode logicalNode) { List specs = logicalNode.getChildren() .stream() .map(node -> createSpecification(node)) .filter(Objects::nonNull) .collect(Collectors.toList()); Specification result = specs.get(0); if (logicalNode.getOperator() == LogicalOperator.AND) { for (int i = 1; i < specs.size(); i++) { result = Specification.where(result).and(specs.get(i)); } } else if (logicalNode.getOperator() == LogicalOperator.OR) { for (int i = 1; i < specs.size(); i++) { result = Specification.where(result).or(specs.get(i)); } } return result; } public Specification createSpecification(ComparisonNode comparisonNode) { Specification result = Specification.where( new GenericRsqlSpecification( comparisonNode.getSelector(), comparisonNode.getOperator(), comparisonNode.getArguments() ) ); return result; } }

Beachten Sie, wie:

  • LogicalNode ist ein UND / ODER- Knoten und hat mehrere untergeordnete Knoten
  • ComparisonNode hat keine untergeordneten Knoten und enthält den Selector, den Operator und die Argumente

Zum Beispiel haben wir für eine Abfrage " name == john ":

  1. Auswahl : "Name"
  2. Operator : "=="
  3. Argumente : [John]

4. Erstellen Sie eine benutzerdefinierte Spezifikation

Bei der Erstellung der Abfrage haben wir eine Spezifikation verwendet:

public class GenericRsqlSpecification implements Specification { private String property; private ComparisonOperator operator; private List arguments; @Override public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { List args = castArguments(root); Object argument = args.get(0); switch (RsqlSearchOperation.getSimpleOperator(operator)) { case EQUAL: { if (argument instanceof String) { return builder.like(root.get(property), argument.toString().replace('*', '%')); } else if (argument == null) { return builder.isNull(root.get(property)); } else { return builder.equal(root.get(property), argument); } } case NOT_EQUAL: { if (argument instanceof String) { return builder.notLike(root. get(property), argument.toString().replace('*', '%')); } else if (argument == null) { return builder.isNotNull(root.get(property)); } else { return builder.notEqual(root.get(property), argument); } } case GREATER_THAN: { return builder.greaterThan(root. get(property), argument.toString()); } case GREATER_THAN_OR_EQUAL: { return builder.greaterThanOrEqualTo(root. get(property), argument.toString()); } case LESS_THAN: { return builder.lessThan(root. get(property), argument.toString()); } case LESS_THAN_OR_EQUAL: { return builder.lessThanOrEqualTo(root. get(property), argument.toString()); } case IN: return root.get(property).in(args); case NOT_IN: return builder.not(root.get(property).in(args)); } return null; } private List castArguments(final Root root) { Class type = root.get(property).getJavaType(); List args = arguments.stream().map(arg -> { if (type.equals(Integer.class)) { return Integer.parseInt(arg); } else if (type.equals(Long.class)) { return Long.parseLong(arg); } else { return arg; } }).collect(Collectors.toList()); return args; } // standard constructor, getter, setter }

Beachten Sie, dass die Spezifikation Generika verwendet und nicht an eine bestimmte Entität (z. B. den Benutzer) gebunden ist.

Weiter - hier ist unsere Aufzählung „ RsqlSearchOperation , die Standard-rsql-Parser-Operatoren enthält:

public enum RsqlSearchOperation { EQUAL(RSQLOperators.EQUAL), NOT_EQUAL(RSQLOperators.NOT_EQUAL), GREATER_THAN(RSQLOperators.GREATER_THAN), GREATER_THAN_OR_EQUAL(RSQLOperators.GREATER_THAN_OR_EQUAL), LESS_THAN(RSQLOperators.LESS_THAN), LESS_THAN_OR_EQUAL(RSQLOperators.LESS_THAN_OR_EQUAL), IN(RSQLOperators.IN), NOT_IN(RSQLOperators.NOT_IN); private ComparisonOperator operator; private RsqlSearchOperation(ComparisonOperator operator) { this.operator = operator; } public static RsqlSearchOperation getSimpleOperator(ComparisonOperator operator) { for (RsqlSearchOperation operation : values()) { if (operation.getOperator() == operator) { return operation; } } return null; } }

5. Testen Sie Suchanfragen

Beginnen wir nun damit, unsere neuen und flexiblen Abläufe anhand einiger realer Szenarien zu testen:

Zuerst - lassen Sie uns die Daten initialisieren:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceConfig.class }) @Transactional @TransactionConfiguration public class RsqlTest { @Autowired private UserRepository repository; private User userJohn; private User userTom; @Before public void init() { userJohn = new User(); userJohn.setFirstName("john"); userJohn.setLastName("doe"); userJohn.setEmail("[email protected]"); userJohn.setAge(22); repository.save(userJohn); userTom = new User(); userTom.setFirstName("tom"); userTom.setLastName("doe"); userTom.setEmail("[email protected]"); userTom.setAge(26); repository.save(userTom); } }

Lassen Sie uns nun die verschiedenen Operationen testen:

5.1. Gleichheit testen

Im folgenden Beispiel suchen wir nach Benutzern anhand ihres Vor- und Nachnamens :

@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName==john;lastName==doe"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

5.2. Test Negation

Als nächstes suchen wir nach Benutzern, die mit ihrem Vornamen nicht "John" sind:

@Test public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName!=john"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }

5.3. Test größer als

Als nächstes suchen wir nach Benutzern mit einem Alter über 25 Jahren :

@Test public void givenMinAge_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("age>25"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }

5.4. Test wie

Als nächstes suchen wir nach Benutzern, deren Vorname mit " jo " beginnt :

@Test public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName==jo*"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

5.5. Test IN

Als nächstes werden wir nach Benutzern suchen, deren Vorname " John " oder " Jack " ist:

@Test public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

6. UserController

Zum Schluss - lassen Sie uns alles mit dem Controller verknüpfen:

@RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List findAllByRsql(@RequestParam(value = "search") String search) { Node rootNode = new RSQLParser().parse(search); Specification spec = rootNode.accept(new CustomRsqlVisitor()); return dao.findAll(spec); }

Hier ist eine Beispiel-URL:

//localhost:8080/users?search=firstName==jo*;age<25

Und die Antwort:

[{ "id":1, "firstName":"john", "lastName":"doe", "email":"[email protected]", "age":24 }]

7. Fazit

This tutorial illustrated how to build out a Query/Search Language for a REST API without having to re-invent the syntax and instead using FIQL / RSQL.

The full implementation of this article can be found in the GitHub project – this is a Maven-based project, so it should be easy to import and run as it is.

Next » REST Query Language with Querydsl Web Support « Previous REST Query Language – Implementing OR Operation REST bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE Persistence bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE