REST-Abfragesprache mit RSQL
1. Überblick
In diesem fünften Artikel vonthe series wird das Erstellen der REST-API-Abfragesprache mithilfe vona cool library – rsql-parser. veranschaulicht
RSQL ist eine Supermenge 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 der Bibliothek zunächst eine Maven-Abhängigkeit hinzu:
cz.jirutka.rsql
rsql-parser
2.0.0
Und auchdefine the main entity, mit denen wir in den Beispielen arbeiten werden -User:
@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 Auswerten der Eingabe verwendet.
In diesem Sinne werden wir dieRSQLVisitor interface implementieren und unsere eigene Besucherimplementierung erstellen -CustomRsqlVisitor:
public class CustomRsqlVisitor implements RSQLVisitor, Void> {
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-Spezifikationenwe used before verwenden - und wir werden einenSpecification Builder fürconstruct Specifications out of each of these nodes we visit implementieren:
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 einAND/ * OR *Node und hat mehrere Kinder
-
ComparisonNode hat keine Kinder und enthältSelector, Operator and the Arguments
Zum Beispiel haben wir für eine Abfrage "name==john":
-
Selector: "Name"
-
Operator: "=="
-
Arguments: [John]
4. Erstellen Sie benutzerdefinierteSpecification
Bei der Erstellung der Abfrage haben wirSpecification: 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
Beachten Sie, dass die Spezifikation Generika verwendet und nicht an eine bestimmte Entität (z. B. den Benutzer) gebunden ist.
Weiter - hier ist unserenum “RsqlSearchOperation“, das 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. Suchanfragen testen
Lassen Sie uns nun unsere neuen und flexiblen Abläufe anhand einiger realer Szenarien testen:
Zuerst - initialisieren wir die Daten:
@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);
}
}
Testen wir nun die verschiedenen Operationen:
5.1. Gleichheit testen
Im folgenden Beispiel suchen wir nach Benutzern anhand ihrerfirst undlast name:
@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 nachfirst namenicht "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 mitage größer als "25":
@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, derenfirst name mit "jo" beginnen:
@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 suchen wir nach Benutzern, derenfirst name "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:
http://localhost:8080/users?search=firstName==jo*;age<25
Und die Antwort:
[{
"id":1,
"firstName":"john",
"lastName":"doe",
"email":"[email protected]",
"age":24
}]
7. Fazit
In diesem Lernprogramm wird veranschaulicht, wie eine Abfrage- / Suchsprache für eine REST-API erstellt wird, ohne dass die Syntax neu erfunden werden muss und stattdessen FIQL / RSQL verwendet wird.
Diefull implementation dieses Artikels befinden sich inthe GitHub project - dies ist ein Maven-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.