项目作者: perplexhub

项目描述 :
Java Library to Translate RSQL into Spring Data JPA Specification and QueryDSL Predicate
高级语言: Java
项目地址: git://github.com/perplexhub/rsql-jpa-specification.git
创建时间: 2018-05-29T08:08:03Z
项目社区:https://github.com/perplexhub/rsql-jpa-specification

开源协议:MIT License

下载


rsql-jpa-specification

Sonatype Nexus (Releases)
Sonatype Nexus (Snapshots)

Release Workflow Status
Snapshot Workflow Status
PR Workflow Status

Translate RSQL query into org.springframework.data.jpa.domain.Specification or com.querydsl.core.types.Predicate and support entities association query.

SpringBoot 3 Support

rsql-jpa-specification supports SpringBoot 3 since version 6.x. (Contributed by chriseteka)

For SpringBoot 2 users, please continue to use version 5.x.

Supported Operators

Since version 5.0.5, you can define your own operators and customize the logic via RSQLCustomPredicate.

Maven Repository

https://oss.sonatype.org/#nexus-search;gav~io.github.perplexhub~rsql*

Add rsql-jpa-spring-boot-starter for RSQL to Spring JPA translation

Maven dependency for rsql-jpa-spring-boot-starter

  1. <dependency>
  2. <groupId>io.github.perplexhub</groupId>
  3. <artifactId>rsql-jpa-spring-boot-starter</artifactId>
  4. <version>X.X.X</version>
  5. </dependency>

Add JpaSpecificationExecutor to your JPA repository interface classes

  1. package com.perplexhub.repository;
  2. import org.springframework.data.jpa.repository.JpaRepository;
  3. import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
  4. import com.perplexhub.model.User;
  5. public interface UserRepository extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
  6. }

Sample main class - Application.java

  1. package io.github.perplexhub.rsql;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
  5. import org.springframework.transaction.annotation.EnableTransactionManagement;
  6. @EnableJpaRepositories(basePackages = { "io.github.xxx.yyy.repository" })
  7. @EnableTransactionManagement
  8. @SpringBootApplication
  9. public class Application {
  10. public static void main(String[] args) throws Exception {
  11. SpringApplication.run(Application.class, args);
  12. }
  13. }

Add rsql-querydsl-spring-boot-starter for RSQL to Spring JPA and QueryDSL translation

Maven dependency for rsql-querydsl-spring-boot-starter

  1. <dependency>
  2. <groupId>io.github.perplexhub</groupId>
  3. <artifactId>rsql-querydsl-spring-boot-starter</artifactId>
  4. <version>X.X.X</version>
  5. </dependency>

Add JpaSpecificationExecutor and QuerydslPredicateExecutor to your JPA repository interface classes

  1. package com.perplexhub.repository;
  2. import org.springframework.data.jpa.repository.JpaRepository;
  3. import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
  4. import org.springframework.data.querydsl.QuerydslPredicateExecutor;
  5. import com.perplexhub.model.User;
  6. public interface UserRepository extends JpaRepository<User, String>, JpaSpecificationExecutor<User>, QuerydslPredicateExecutor<User> {
  7. }

Use below properties to control the version of Spring Boot, Spring Data and QueryDSL

  1. <properties>
  2. <spring-boot.version>3.0.0</spring-boot.version>
  3. <spring-data-releasetrain.version>2022.0.0</spring-data-releasetrain.version>
  4. <querydsl.version>4.1.4</querydsl.version>
  5. </properties>

RSQL Syntax Reference

  1. filter = "id=bt=(2,4)";// id>=2 && id<=4 //between
  2. filter = "id=nb=(2,4)";// id<2 || id>4 //not between
  3. filter = "company.code=like=em"; //like %em%
  4. filter = "company.code=ilike=EM"; //ignore case like %EM%
  5. filter = "company.code=icase=EM"; //ignore case equal EM
  6. filter = "company.code=notlike=em"; //not like %em%
  7. filter = "company.code=inotlike=EM"; //ignore case not like %EM%
  8. filter = "company.code=ke=e*m"; //like %e*m%
  9. filter = "company.code=ik=E*M"; //ignore case like %E*M%
  10. filter = "company.code=nk=e*m"; //not like %e*m%
  11. filter = "company.code=ni=E*M"; //ignore case not like %E*M%
  12. filter = "company.code=ic=E^^M"; //ignore case equal E^^M
  13. filter = "company.code==demo"; //equal
  14. filter = "company.code=='demo'"; //equal
  15. filter = "company.code==''"; //equal to empty string
  16. filter = "company.code==dem*"; //like dem%
  17. filter = "company.code==*emo"; //like %emo
  18. filter = "company.code==*em*"; //like %em%
  19. filter = "company.code==^EM"; //ignore case equal EM
  20. filter = "company.code==^*EM*"; //ignore case like %EM%
  21. filter = "company.code=='^*EM*'"; //ignore case like %EM%
  22. filter = "company.code!=demo"; //not equal
  23. filter = "company.code=in=(*)"; //equal to *
  24. filter = "company.code=in=(^)"; //equal to ^
  25. filter = "company.code=in=(demo,real)"; //in
  26. filter = "company.code=out=(demo,real)"; //not in
  27. filter = "company.id=gt=100"; //greater than
  28. filter = "company.id=lt=100"; //less than
  29. filter = "company.id=ge=100"; //greater than or equal
  30. filter = "company.id=le=100"; //less than or equal
  31. filter = "company.id>100"; //greater than
  32. filter = "company.id<100"; //less than
  33. filter = "company.id>=100"; //greater than or equal
  34. filter = "company.id<=100"; //less than or equal
  35. filter = "company.code=isnull="; //is null
  36. filter = "company.code=null="; //is null
  37. filter = "company.code=na="; //is null
  38. filter = "company.code=nn="; //is not null
  39. filter = "company.code=notnull="; //is not null
  40. filter = "company.code=isnotnull="; //is not null
  41. filter = "company.code=='demo';company.id>100"; //and
  42. filter = "company.code=='demo' and company.id>100"; //and
  43. filter = "company.code=='demo',company.id>100"; //or
  44. filter = "company.code=='demo' or company.id>100"; //or

Syntax Reference: RSQL / FIQL parser

Spring Data JPA Specification

  1. Pageable pageable = PageRequest.of(0, 5); //page 1 and page size is 5
  2. repository.findAll(RSQLSupport.toSpecification(filter));
  3. repository.findAll(RSQLSupport.toSpecification(filter), pageable);
  4. repository.findAll(RSQLSupport.toSpecification(filter, true)); // select distinct
  5. repository.findAll(RSQLSupport.toSpecification(filter, true), pageable);
  6. // use static import
  7. import static io.github.perplexhub.rsql.RSQLSupport.*;
  8. repository.findAll(toSpecification(filter));
  9. repository.findAll(toSpecification(filter), pageable);
  10. repository.findAll(toSpecification(filter, true)); // select distinct
  11. repository.findAll(toSpecification(filter, true), pageable);
  12. // property path remap
  13. filter = "compCode=='demo';compId>100"; // "company.code=='demo';company.id>100" - protect our domain model #10
  14. Map<String, String> propertyPathMapper = new HashMap<>();
  15. propertyPathMapper.put("compId", "company.id");
  16. propertyPathMapper.put("compCode", "company.code");
  17. repository.findAll(toSpecification(filter, propertyPathMapper));
  18. repository.findAll(toSpecification(filter, propertyPathMapper), pageable);

Sort Syntax

  1. sort = "id"; // order by id asc
  2. sort = "id,asc"; // order by id asc
  3. sort = "id,asc;company.id,desc"; // order by id asc, company.id desc
  4. sort = "name,asc,ic" // order by name ascending ignore case

Sort with JPA Specifications

  1. repository.findAll(RSQLSupport.toSort("id,asc;company.id,desc"));
  2. // sort with custom field mapping
  3. Map<String, String> propertyMapping = new HashMap<>();
  4. propertyMapping.put("userID", "id");
  5. propertyMapping.put("companyID", "company.id");
  6. repository.findAll(RSQLSupport.toSort("userID,asc;companyID,desc", propertyMapping)); // same as id,asc;company.id,desc

Filtering and Sorting with JPA Specification

  1. Specification<?> specification = RSQLSupport.toSpecification("company.name==name")
  2. .and(RSQLSupport.toSort("company.name,asc,ic;user.id,desc"));
  3. repository.findAll(specification);

QueryDSL Predicate (BooleanExpression)

  1. Pageable pageable = PageRequest.of(0, 5); //page 1 and page size is 5
  2. repository.findAll(RSQLSupport.toPredicate(filter, QUser.user));
  3. repository.findAll(RSQLSupport.toPredicate(filter, QUser.user), pageable);
  4. // use static import
  5. import static io.github.perplexhub.rsql.RSQLSupport.*;
  6. repository.findAll(toPredicate(filter, QUser.user));
  7. repository.findAll(toPredicate(filter, QUser.user), pageable);
  8. // property path remap
  9. filter = "compCode=='demo';compId>100"; // "company.code=='demo';company.id>100" - protect our domain model #10
  10. Map<String, String> propertyPathMapper = new HashMap<>();
  11. propertyPathMapper.put("compId", "company.id");
  12. propertyPathMapper.put("compCode", "company.code");
  13. repository.findAll(toPredicate(filter, QUser.user, propertyPathMapper));
  14. repository.findAll(toPredicate(filter, QUser.user, propertyPathMapper), pageable);

Custom Value Converter

  1. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
  2. RSQLJPASupport.addConverter(Date.class, s -> {
  3. try {
  4. return sdf.parse(s);
  5. } catch (ParseException e) {
  6. return null;
  7. }
  8. });

Custom Operator & Predicate

  1. String rsql = "createDate=dayofweek='2'";
  2. RSQLCustomPredicate<Long> customPredicate = new RSQLCustomPredicate<>(new ComparisonOperator("=dayofweek="), Long.class, input -> {
  3. Expression<Long> function = input.getCriteriaBuilder().function("ISO_DAY_OF_WEEK", Long.class, input.getPath());
  4. return input.getCriteriaBuilder().lessThan(function, (Long) input.getArguments().get(0));
  5. });
  6. List<User> users = userRepository.findAll(toSpecification(rsql, Arrays.asList(customPredicate)));
  1. String rsql = "name=around='May'";
  2. RSQLCustomPredicate<String> customPredicate = new RSQLCustomPredicate<>(new ComparisonOperator("=around="), String.class, input -> {
  3. if ("May".equals(input.getArguments().get(0))) {
  4. return input.getPath().in(Arrays.asList("April", "May", "June"));
  5. }
  6. return input.getCriteriaBuilder().equal(input.getPath(), (String) input.getArguments().get(0));
  7. });
  8. List<User> users = userRepository.findAll(toSpecification(rsql, Arrays.asList(customPredicate)));
  1. String rsql = "company.id=between=(2,3)";
  2. RSQLCustomPredicate<Long> customPredicate = new RSQLCustomPredicate<>(new ComparisonOperator("=between=", true), Long.class, input -> {
  3. return input.getCriteriaBuilder().between(input.getPath().as(Long.class), (Long) input.getArguments().get(0), (Long) input.getArguments().get(1));
  4. });
  5. List<User> users = userRepository.findAll(toSpecification(rsql, Arrays.asList(customPredicate)));
  1. String rsql = "city=notAssigned=''";
  2. RSQLCustomPredicate<String> customPredicate = new RSQLCustomPredicate<>(new ComparisonOperator("=notAssigned="), String.class, input -> {
  3. return input.getCriteriaBuilder().isNull(input.getRoot().get("city"));
  4. });
  5. List<User> users = userRepository.findAll(toSpecification(rsql, Arrays.asList(customPredicate)));

Escaping Special Characters in LIKE Predicate

For the LIKE statement in different RDBMS, the most commonly used special characters are:

MySQL/MariaDB and PostgreSQL

  • %: Represents any sequence of zero or more characters. For example, LIKE '%abc' would match any string ending with abc.
  • _: Represents any single character. For example, LIKE 'a_c' would match a three-character string starting with a and ending with c.

SQL Server

  • % and _: Function in the same way as in MySQL/MariaDB and PostgreSQL.
  • []: Used to specify a set or range of characters. For instance, LIKE '[a-c]%' would match any string starting with a, b, or c.
  • ^: Used within [] to exclude characters. For example, LIKE ‘[^a-c]%’ would match any string not starting with a, b, or c.

Oracle:

  • % and _: Function similarly to MySQL/MariaDB and PostgreSQL.
  • ESCAPE: Allows specifying an escape character to include % or literally in the search. For example, `LIKE ‘%\%’ ESCAPE ‘\’` would match a string containing an underscore.

LIKE in RSQL

To use escape character in RSQL, you must use QuerySupport to build the Specification with appropriate escape character.

  1. char escapeChar = '$';
  2. QuerySupport query = QuerySupport.builder()
  3. .rsqlQuery("name=like='" + escapeChar + "%'")
  4. .likeEscapeCharacter(escapeChar)
  5. .build();
  6. List<Company> users = companyRepository.findAll(toSpecification(query));

Example

Above RSQL with default escape character $ for searching string containing _:

  1. my_table.my_column=like='$_'

Will produce the following SQL:

  1. SELECT * FROM my_table WHERE my_column LIKE '%$_%' ESCAPE '$'

Jsonb Support with Postgresql

It’s possible to make rsql queries on jsonb fields. For example, if you have a jsonb field named data in your entity, you can make queries like this:

  1. {
  2. "data": {
  3. "name": "demo",
  4. "user" : {
  5. "id": 1,
  6. "name": "demo"
  7. },
  8. "roles": [
  9. {
  10. "id": 1,
  11. "name": "admin"
  12. },
  13. {
  14. "id": 2,
  15. "name": "user"
  16. }
  17. ]
  18. }
  19. }
  1. String rsql = "data.name==demo";
  2. List<User> users = userRepository.findAll(toSpecification(rsql));
  1. String rsql = "data.user.id==1";
  2. List<User> users = userRepository.findAll(toSpecification(rsql));
  1. String rsql = "data.roles.id==1";
  2. List<User> users = userRepository.findAll(toSpecification(rsql));

The library use jsonb_path_exists function under the hood.
Json primitive types are supported such as

  • string
  • number
  • boolean
  • array

Temporal values support

Since Postgresql 13 jsonb supports temporal values with datetime() function.
As Date time values are string in jsonb, you can make queries on them as well.
You must use the ISO 8601 format for date time values.

If your request conform timezone pattern, the library will use `jsonb_path_exists_tz.
Then consider the timezone consideration of the official documentation

Stored procedure

RSQL can call a stored procedure with the following syntax for both search and sort.
In order to be authorized to call a stored procedure, it must be whitelisted and not blacklisted.
The only way to whitelist or blacklist a stored procedure is to use the QuerySupport when performing the search or the SortSupport when performing the sort.

  1. String rsql = "@concat[greetings|#123]=='HELLO123'";
  2. QuerySupport querySupport = QuerySupport.builder()
  3. .rsqlQuery(rsql)
  4. .procedureWhiteList(List.of("concat", "upper"))
  5. .build();
  6. List<Item> companies = itemRepository.findAll(toSpecification(querySupport));

Regex like expression can be used to whitelist or blacklist stored procedure.

Syntax

A procedure must be prefixed with @ and called with [] for arguments.

  1. @procedure_name[arg1|arg2|...]

Arguments

Arguments are separated by | and can be:

  • constant (null, boolean, number, string), prefixed with #
  • column name
  • other procedure call
  1. @procedure_name[arg1|arg2|...]
  2. @procedure_name[column1|column2|...]
  3. @procedure_name[@function_name[arg1|arg2|...]|column1|#textvalue|#123|#true|#false|#null]

For text value, since space is not supported by RSQL, you can use \t to replace space.

Usage

  1. String rsql1 = "@upper[code]==HELLO";
  2. String rsql2 = "@concat[@upper[code]|name]=='TESTTest Lab'";
  3. String rsql3 = "@concat[@upper[code]|#123]=='HELLO123'";

Sort

  1. String sort1 = "@upper[code],asc";
  2. String sort2 = "@concat[@upper[code]|name],asc";
  3. String sort3 = "@concat[@upper[code]|#123],asc";