REST Query Language met RSQL

REST Top

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS Persistentie top

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS Dit artikel maakt deel uit van een reeks: • REST Query Language met Spring- en JPA-criteria

• REST Query Language met Spring Data JPA-specificaties

• REST Query Language met Spring Data JPA en Querydsl

• REST Query Language - Geavanceerde zoekbewerkingen

• REST Query Language - OR-bewerking implementeren

• REST Query Language met RSQL (huidig ​​artikel) • REST Query Language met Querydsl Web Support

1. Overzicht

In dit vijfde artikel van de serie illustreren we het bouwen van de REST API Query-taal met behulp van een coole bibliotheek - rsql-parser.

RSQL is een superset van de Feed Item Query Language (FIQL) - een schone en eenvoudige filtersyntaxis voor feeds; dus het past heel natuurlijk in een REST API.

2. Voorbereidingen

Laten we eerst een maven-afhankelijkheid aan de bibliotheek toevoegen:

 cz.jirutka.rsql rsql-parser 2.1.0 

En ook definieer de hoofdentiteit we gaan door de voorbeelden heen werken - Gebruiker:

@Entity openbare klasse Gebruiker {@Id @GeneratedValue (strategie = GenerationType.AUTO) privé Lange id; private String voornaam; private String achternaam; privé String-e-mail; privé int leeftijd; }

3. Analyseer het verzoek

De manier waarop RSQL-expressies intern worden weergegeven, is in de vorm van knooppunten en het bezoekerspatroon wordt gebruikt om de invoer te analyseren.

Met dat in gedachten gaan we het RSQLBezoeker interface en maak onze eigen bezoekersimplementatie - CustomRsqlVisitor:

openbare klasse CustomRsqlVisitor implementeert RSQLVisitor {privé GenericRsqlSpecBuilder-bouwer; openbare CustomRsqlVisitor () {builder = nieuwe 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); }}

Nu moeten we omgaan met persistentie en onze vraag construeren uit elk van deze knooppunten.

We gaan de Spring Data JPA-specificaties gebruiken die we eerder hebben gebruikt - en we gaan een Specificatie bouwer aan construeer specificaties uit elk van deze knooppunten die we bezoeken:

openbare klasse GenericRsqlSpecBuilder {openbare specificatie createSpecification (knooppuntknooppunt) {if (knooppuntinstantie van LogicalNode) {terugkeer createSpecification ((LogicalNode) knooppunt); } if (knooppunt instanceof ComparisonNode) {return createSpecification ((ComparisonNode) knooppunt); } retourneer null; } openbare specificatie createSpecification (LogicalNode logicalNode) {Lijstspecificaties = logicalNode.getChildren () .stream () .map (knooppunt -> createSpecification (knooppunt)) .filter (Objects :: nonNull) .collect (Collectors.toList ()); Specificatie resultaat = specs.get (0); if (logicalNode.getOperator () == LogicalOperator.AND) {for (int i = 1; i <specs.size (); i ++) {result = Specification.where (resultaat) .en (specs.get (i)) ; }} else if (logicalNode.getOperator () == LogicalOperator.OR) {for (int i = 1; i <specs.size (); i ++) {result = Specification.where (resultaat) .or (specs.get ( ik)); }} resultaat retourneren; } openbare Specificatie createSpecification (ComparisonNode comparisonNode) {Specificatie resultaat = Specification.where (nieuwe GenericRsqlSpecification (comparisonNode.getSelector (), comparisonNode.getOperator (), comparisonNode.getArguments ())); resultaat teruggeven; }}

Merk op hoe:

  • LogicalNode is een EN/OFKnooppunt en heeft meerdere kinderen
  • ComparisonNode heeft geen kinderen en het bevat de Selector, Operator en de argumenten

Voor een zoekopdracht 'naam == john" - we hebben:

  1. Selector: "naam"
  2. Operator: “==”
  3. Argumenten:[John]

4. Maak een aangepast Specificatie

Bij het samenstellen van de query hebben we gebruik gemaakt van een Specificatie:

openbare klasse GenericRsqlSpecification implementeert Specification {private String-eigenschap; privé ComparisonOperator-operator; argumenten voor privélijst; @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 (eigenschap)); } else {return builder.equal (root.get (eigenschap), argument); }} geval NOT_EQUAL: {if (argument instanceof String) {return builder.notLike (root. get (property), argument.toString (). replace ('*', '%')); } else if (argument == null) {return builder.isNotNull (root.get (eigenschap)); } else {return builder.notEqual (root.get (eigenschap), argument); }} geval GREATER_THAN: {return builder.greaterThan (root. get (eigenschap), argument.toString ()); } geval GREATER_THAN_OR_EQUAL: {return builder.greaterThanOrEqualTo (root. get (eigenschap), argument.toString ()); } case LESS_THAN: {return builder.lessThan (root. get (eigenschap), argument.toString ()); } case LESS_THAN_OR_EQUAL: {return builder.lessThanOrEqualTo (root. get (eigenschap), argument.toString ()); } case IN: retourneer root.get (eigenschap) .in (args); case NOT_IN: return builder.not (root.get (property) .in (args)); } retourneer null; } privélijst castArguments (laatste root root) {Class type = root.get (eigenschap) .getJavaType (); Lijst 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 ()); args teruggeven; } // standard constructor, getter, setter}

Merk op hoe de specificatie generieke geneesmiddelen gebruikt en niet is gekoppeld aan een specifieke entiteit (zoals de gebruiker).

Volgende - hier is onze opsomming 'RsqlSearchOperation die standaard rsql-parser-operators bevat:

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); privé ComparisonOperator-operator; privé RsqlSearchOperation (operator ComparisonOperator) {this.operator = operator; } openbare statische RsqlSearchOperation getSimpleOperator (operator ComparisonOperator) {for (bewerking RsqlSearchOperation: waarden ()) {if (operation.getOperator () == operator) {retouroperatie; }} retourneer null; }}

5. Test zoekopdrachten

Laten we nu beginnen met het testen van onze nieuwe en flexibele operaties aan de hand van enkele realistische scenario's:

Laten we eerst de gegevens initialiseren:

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (classes = {PersistenceConfig.class}) @Transactional @TransactionConfiguration openbare klasse RsqlTest {@Autowired private UserRepository-repository; privé gebruiker userJohn; particuliere gebruiker userTom; @Before public void init () {userJohn = nieuwe gebruiker (); userJohn.setFirstName ("john"); userJohn.setLastName ("doe"); userJohn.setEmail ("[e-mail beschermd]"); userJohn.setAge (22); repository.save (userJohn); userTom = nieuwe gebruiker (); userTom.setFirstName ("tom"); userTom.setLastName ("doe"); userTom.setEmail ("[e-mail beschermd]"); userTom.setAge (26); repository.save (userTom); }}

Laten we nu de verschillende bewerkingen testen:

5.1. Test gelijkheid

In het volgende voorbeeld zoeken we gebruikers op basis van hun eerste en achternaam:

@Test openbare leegte gegevenFirstAndLastName_whenGettingListOfUsers_thenCorrect () {Node rootNode = nieuwe RSQLParser (). Parse ("firstName == john; lastName == doe"); Specificatie spec = rootNode.accept (nieuwe CustomRsqlVisitor ()); Lijstresultaten = repository.findAll (spec); assertThat (userJohn, isIn (resultaten)); assertThat (userTom, niet (isIn (resultaten))); }

5.2. Test ontkenning

Laten we vervolgens naar gebruikers zoeken die worden aangeduid met hun Voornaam niet "john":

@Test openbare ongeldige gegevenFirstNameInverse_whenGettingListOfUsers_thenCorrect () {Node rootNode = nieuwe RSQLParser (). Parse ("firstName! = John"); Specificatie spec = rootNode.accept (nieuwe CustomRsqlVisitor ()); Lijstresultaten = repository.findAll (spec); assertThat (userTom, isIn (resultaten)); assertThat (userJohn, not (isIn (results))); }

5.3. Test groter dan

Vervolgens zullen we zoeken naar gebruikers met leeftijd groter dan "25”:

@Test openbare ongeldig gegevenMinAge_whenGettingListOfUsers_thenCorrect () {Node rootNode = nieuwe RSQLParser (). Parse ("leeftijd> 25"); Specificatie spec = rootNode.accept (nieuwe CustomRsqlVisitor ()); Lijstresultaten = repository.findAll (spec); assertThat (userTom, isIn (resultaten)); assertThat (userJohn, not (isIn (results))); }

5.4. Test zoals

Vervolgens zullen we zoeken naar gebruikers met hun Voornaam beginnend met "jo”:

@Test openbare ongeldige gegevenFirstNamePrefix_whenGettingListOfUsers_thenCorrect () {Node rootNode = nieuwe RSQLParser (). Parse ("firstName == jo *"); Specificatie spec = rootNode.accept (nieuwe CustomRsqlVisitor ()); Lijstresultaten = repository.findAll (spec); assertThat (userJohn, isIn (resultaten)); assertThat (userTom, niet (isIn (resultaten))); }

5.5. Test IN

Vervolgens zullen we zoeken naar gebruikers hun Voornaam is "John"Of"jack“:

@Test openbare ongeldig gegevenListOfFirstName_whenGettingListOfUsers_thenCorrect () {Node rootNode = nieuwe RSQLParser (). Parse ("firstName = in = (john, jack)"); Specificatie spec = rootNode.accept (nieuwe CustomRsqlVisitor ()); Lijstresultaten = repository.findAll (spec); assertThat (userJohn, isIn (resultaten)); assertThat (userTom, niet (isIn (resultaten))); }

6. UserController

Eindelijk - laten we het allemaal in verband brengen met de controller:

@RequestMapping (methode = RequestMethod.GET, value = "/ gebruikers") @ResponseBody openbare lijst findAllByRsql (@RequestParam (waarde = "zoeken") String zoeken) {Knooppunt rootNode = nieuwe RSQLParser (). Parse (zoeken); Specificatie spec = rootNode.accept (nieuwe CustomRsqlVisitor ()); retourneer dao.findAll (spec); }

Hier is een voorbeeld-URL:

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

En het antwoord:

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

7. Conclusie

Deze tutorial illustreerde hoe je een Query / Search Language voor een REST API opbouwt zonder de syntaxis opnieuw uit te vinden en in plaats daarvan FIQL / RSQL te gebruiken.

De volledige implementatie van dit artikel is te vinden in het GitHub-project - dit is een op Maven gebaseerd project, dus het zou gemakkelijk te importeren en uit te voeren moeten zijn zoals het is.

De volgende » REST Query-taal met Querydsl-webondersteuning « Vorige REST-querytaal - Implementatie van OR-bewerking REST onderaan

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS Persistentie onderaan

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS