REST Query Language dengan RSQL

REST Top

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> LIHAT KURSUS Ketekunan atas

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> PERIKSA KURSUS Artikel ini adalah bagian dari seri: • REST Query Language dengan Kriteria Spring dan JPA

• Bahasa Kueri REST dengan Spesifikasi JPA Spring Data

• REST Query Language dengan Spring Data JPA dan Querydsl

• REST Query Language - Operasi Pencarian Lanjutan

• REST Query Language - Menerapkan ATAU Operasi

• REST Query Language dengan RSQL (artikel saat ini) • REST Query Language dengan Querydsl Web Support

1. Ikhtisar

Dalam artikel kelima dari seri ini, kami akan mengilustrasikan pembuatan bahasa Kueri REST API dengan bantuan pustaka keren - rsql-parser.

RSQL adalah kumpulan super dari Feed Item Query Language (FIQL) - sintaks filter yang bersih dan sederhana untuk feed; sehingga cocok secara alami dengan REST API.

2. Persiapan

Pertama, mari tambahkan dependensi maven ke library:

 cz.jirutka.rsql rsql-parser 2.1.0 

Dan juga tentukan entitas utama yang akan kita kerjakan di seluruh contoh - Pengguna :

@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. Parse Permintaan

Ekspresi RSQL direpresentasikan secara internal dalam bentuk node dan pola pengunjung digunakan untuk mengurai input.

Dengan pemikiran tersebut, kita akan mengimplementasikan antarmuka RSQLVisitor dan membuat implementasi pengunjung kita sendiri - 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); } }
     

Sekarang kita perlu berurusan dengan ketekunan dan membuat kueri dari masing-masing node ini.

Kami akan menggunakan Spesifikasi JPA Data Musim Semi yang kami gunakan sebelumnya - dan kami akan menerapkan pembuat Spesifikasi untuk membuat Spesifikasi dari setiap node yang kami kunjungi :

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; } }

Perhatikan caranya:

  • LogicalNode adalah Node AND / OR dan memiliki banyak turunan
  • ComparisonNode tidak memiliki turunan dan memegang Selector, Operator, dan Arguments

Misalnya, untuk kueri " name == john " - kami memiliki:

  1. Pemilih : "nama"
  2. Operator : “==”
  3. Argumen : [john]

4. Buat Spesifikasi Kustom

Saat membuat kueri, kami menggunakan Spesifikasi:

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 }

Perhatikan bagaimana spesifikasi menggunakan obat generik dan tidak terikat dengan Entitas tertentu (seperti Pengguna).

Berikutnya - inilah enum kami " RsqlSearchOperation " yang menampung operator rsql-parser default:

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. Uji Kueri Penelusuran

Sekarang mari kita mulai menguji operasi baru dan fleksibel kita melalui beberapa skenario dunia nyata:

Pertama - mari kita inisialisasi data:

@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); } }

Sekarang mari kita uji operasi yang berbeda:

5.1. Uji Kesetaraan

Dalam contoh berikut - kami akan mencari pengguna dengan nama depan dan belakang mereka :

@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. Uji Negasi

Selanjutnya, mari cari pengguna yang dengan nama depannya bukan "john":

@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. Uji Lebih Besar Dari

Berikutnya - kami akan menelusuri pengguna dengan usia lebih dari " 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. Uji Suka

Selanjutnya - kita akan mencari pengguna dengan nama depan mereka yang diawali dengan " jo ":

@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. Tes DI

Selanjutnya - kami akan mencari pengguna, nama depan mereka adalah " john " atau " jack ":

@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

Akhirnya - mari kita ikat semuanya dengan pengontrol:

@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); }

Berikut contoh URL:

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

Dan tanggapannya:

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

7. Kesimpulan

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