FIQL یا Feed Item Query Language یک زبان ساختارمند، منعطف، URI friendly برای ساخت عبارات کوئری های سرچ و فیلتر است که توسط کتابخانه RSQL میتوانیم از آن در REST API استفاده کنیم
<dependency> <groupId>cz.jirutka.rsql</groupId> <artifactId>rsql-parser</artifactId> <version>2.0.0</version> </dependency>
یک Entity تعریف میکنیم که بعدا قرار است در مثال ها از آن استفاده کنیم :
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; }
Parse کردن دستورات رسیده از کلاینت :
عبارات RSQL باید توسط مکانیزمی پردازش شوند برای این منظور میتوانیم از اینترفیس RSQLVisitor استفاده کنیم
public class CustomRsqlVisitor<T> implements RSQLVisitor<Specification<T>, Void> { private GenericRsqlSpecBuilder<T> builder; public CustomRsqlVisitor() { builder = new GenericRsqlSpecBuilder<T>(); } @Override public Specification<T> visit(AndNode node, Void param) { return builder.createSpecification(node); } @Override public Specification<T> visit(OrNode node, Void param) { return builder.createSpecification(node); } @Override public Specification<T> visit(ComparisonNode node, Void params) { return builder.createSecification(node); } }
حالا نیاز داریم که بتوانیم با دیتابیس هم تعامل داشته باشیم از اینرو از Spring Data JPA Specification استفاده میکنیم و یک Specification Builder میسازیم :
public class GenericRsqlSpecBuilder<T> { public Specification<T> createSpecification(Node node) { if (node instanceof LogicalNode) { return createSpecification((LogicalNode) node); } if (node instanceof ComparisonNode) { return createSpecification((ComparisonNode) node); } return null; } public Specification<T> createSpecification(LogicalNode logicalNode) { List<Specification> specs = logicalNode.getChildren() .stream() .map(node -> createSpecification(node)) .filter(Objects::nonNull) .collect(Collectors.toList()); Specification<T> 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<T> createSpecification(ComparisonNode comparisonNode) { Specification<T> result = Specification.where( new GenericRsqlSpecification<T>( comparisonNode.getSelector(), comparisonNode.getOperator(), comparisonNode.getArguments() ) ); return result; } }
نکته ای که وجود دارد :
- LogicalNode شامل نود های And یا Or هستند که دارای فرزندانی نیز میباشند
- ComparisonNode فرزندی ندارد و در بر گیرنده Selector ها ، Operator ها و Argument ها میباشد
برای مثال در کوئری “name==john” :
name یک Selector است
== یک Operator است
john یک Argument است
Specification سفارشی :
موقع ساخت کوئری میتوانیم از کلاس زیر استفاده کنیم :
public class GenericRsqlSpecification<T> implements Specification<T> { private String property; private ComparisonOperator operator; private List<String> arguments; @Override public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) { List<Object> 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.<String> 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.<String> get(property), argument.toString()); } case GREATER_THAN_OR_EQUAL: { return builder.greaterThanOrEqualTo(root.<String> get(property), argument.toString()); } case LESS_THAN: { return builder.lessThan(root.<String> get(property), argument.toString()); } case LESS_THAN_OR_EQUAL: { return builder.lessThanOrEqualTo(root.<String> 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<Object> castArguments(final Root<T> root) { Class<? extends Object> type = root.get(property).getJavaType(); List<Object> 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 }
توجه کنید که در کد بالا از Generics استفاده شده است که باعث شده وابسته به عامل دیگری مانند User نباشد و عملیات در RsqlSearchOperation بصورت Enum تعریف شده است که در قبل مشاهده کردیم
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; } }
تست :
قبل از شروع تست چند Entity ذخیره میکنیم :
@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("john@doe.com"); userJohn.setAge(22); repository.save(userJohn); userTom = new User(); userTom.setFirstName("tom"); userTom.setLastName("doe"); userTom.setEmail("tom@doe.com"); userTom.setAge(26); repository.save(userTom); } }
تست کردن Quality :
@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName==john;lastName==doe"); Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>()); List<User> results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
تست Negation :
@Test public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName!=john"); Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>()); List<User> results = repository.findAll(spec); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }
تست Greater Than :
@Test public void givenMinAge_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("age>25"); Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>()); List<User> results = repository.findAll(spec); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }
تست Like :
@Test public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName==jo*"); Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>()); List<User> results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
تست In :
@Test public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)"); Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>()); List<User> results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
ایجاد کلاس Controller :
@RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List<User> findAllByRsql(@RequestParam(value = "search") String search) { Node rootNode = new RSQLParser().parse(search); Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>()); return dao.findAll(spec); }
درخواست به کنترلر :
http://localhost:8080/users?search=firstName==jo*;age<25
نمونه پاسخ دریافتی کلاینت :
[{ "id":1, "firstName":"john", "lastName":"doe", "email":"john@doe.com", "age":24 }]