REST Bahasa Pertanyaan dengan RSQL

REST Teratas

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

>> LIHAT KURSUS Kegigihan atas

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

>> LIHAT KURSUS Artikel ini adalah sebahagian daripada siri: • REST Bahasa Pertanyaan dengan Kriteria Musim Semi dan JPA

• Bahasa Pertanyaan REST dengan Spesifikasi JPA Spring Data

• REST Query Language dengan Spring Data JPA dan Querydsl

• REST Query Language - Operasi Carian Lanjutan

• Bahasa Pertanyaan REST - Melaksanakan ATAU Operasi

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

1. Gambaran keseluruhan

Dalam artikel kelima siri ini, kami akan menggambarkan pembinaan bahasa REST API Query dengan bantuan perpustakaan yang hebat - rsql-parser.

RSQL adalah set super dari Feed Item Query Language (FIQL) - sintaks penapis yang bersih dan sederhana untuk suapan; jadi ia sesuai dengan API REST.

2. Persediaan

Pertama, mari tambah pergantungan maven ke perpustakaan:

 cz.jirutka.rsql rsql-parser 2.1.0 

Dan tentukan juga entiti utama yang akan kita bekerjasama dengan semua 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. Huraikan Permintaan

Cara ekspresi RSQL diwakili secara dalaman adalah dalam bentuk nod dan corak pelawat digunakan untuk menguraikan input.

Dengan ini, kami akan melaksanakan antara muka RSQLVisitor dan membuat pelaksanaan pelawat kami 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 menangani kegigihan dan membina pertanyaan kita dari setiap nod ini.

Kami akan menggunakan Spesifikasi JPA Spring Data yang kami gunakan sebelumnya - dan kami akan melaksanakan pembangun Spesifikasi untuk membina Spesifikasi dari setiap node yang kami lawati :

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 bagaimana:

  • LogicalNode adalah Node AND / OR dan mempunyai banyak anak
  • ComparisonNode tidak mempunyai anak dan memegang Pemilih, Operator dan Hujah

Sebagai contoh, untuk pertanyaan " name == john " - kami mempunyai:

  1. Pemilih : "name"
  2. Pengendali : “==”
  3. Hujah : [john]

4. Buat Spesifikasi Khusus

Semasa membuat pertanyaan, 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 generik dan tidak terikat dengan Entiti tertentu (seperti Pengguna).

Seterusnya - inilah enum kami " RsqlSearchOperation " yang memegang pengendali rsql-parser lalai:

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. Pertanyaan Carian Ujian

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

Pertama - mari kita mulakan 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 berbeza:

5.1. Persamaan Ujian

Dalam contoh berikut - kami akan mencari pengguna-pengguna oleh mereka pertama dan nama akhir :

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

Seterusnya, mari cari pengguna yang dengan nama pertama mereka 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 Daripada

Seterusnya - kami akan mencari 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

Seterusnya - kami akan mencari pengguna dengan nama pertama mereka bermula 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. Uji DALAM

Seterusnya - kami akan mencari pengguna nama pertama 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. PenggunaPengawal

Akhirnya - mari kita mengikat semuanya dengan pengawal:

@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 adalah contoh URL:

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

Dan tindak balas:

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

7. Kesimpulannya

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