QueryDsl-Filters is an open-source library that extends QueryDsl framework functionality with dynamic filters that programmers can use to execute database queries easily.
- Java 17+
- Maven 3.9.5+
- Querydsl 5.0.0+ (classifier: jakarta)
- Lombok 1.18.30+
The first step is to include QueryDsl-Filters into your project. You can download Maven dependency from Maven Central.
<dependency>
<groupId>io.github.squdan</groupId>
<artifactId>querydsl-filters</artifactId>
<version>0.0.2</version>
</dependency>
Once you have the dependency into your project, it requires a minimum configuration to work into your project.
QueryDsl framework needs to generate QueryDsl types from your entities, to enable this generation you must add next plugin to your maven pom.xml. Also, you may need to configure your project to detect generated resources from target/generated-sources/java folder.
<!-- Plugin to generate QueryDsl resources -->
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
To enable QueryDslFilter method in your Spring-Data repositories you have to extends your repositories from QueryDslRepository and define a method returning the entity class. Example:
package io.github.squdan.querydsl.filters.examples;
import io.github.squdan.querydsl.filters.repository.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
public interface ExampleRepository extends JpaRepository<ExampleEntity, ID_TYPE>, QueryDslRepository<ExampleEntity, EntityPathBase<ExampleEntity>> {
// Required configuration
default Class<UserEntity> getEntityType() {
return ExampleEntity.class;
}
}
Extending from QueryDslRepository you repository will offer you a new method findAll that accepts your QueryDslFilters and return founds results.
import org.springframework.data.domain.Pageable;
import io.github.squdan.querydsl.filters.QueryDslFilter;
List<T> findAll(final List<QueryDslFilter> filters, final Pageable pageable);
QueryDsl-Filters supports next types by default:
- Collection: fields inside the collection, not the collection itself.
- Dates: Instant, LocalDate, LocalDateTime, Date, etc...
- Numbers: Integer, BigDecimal, Double, etc...
- UUID
- Strings
You may find some undesirable behaviour at this default type management or you may want to support new types like enums. To achieve this you can configure your custom implementation of QueryDslTypeManager and configure your own behaviour.
package io.github.squdan.querydsl.filters.examples;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.core.types.dsl.SimplePath;
import io.github.squdan.querydsl.filters.QueryDslFilter;
import io.github.squdan.querydsl.filters.QueryDslFiltersException;
import io.github.squdan.querydsl.filters.repository.type.QueryDslTypeManager;
public final class QueryDslExampleEnumTypeManager implements QueryDslTypeManager {
// Conditions to execute this QueryDslTypeManager
public <T> boolean isSupported(final Class<T> entityType, final PathBuilder<T> entityPath, final QueryDslFilter filter) {
return ExampleEnum.class.isAssignableFrom(getTypeFrom(entityType, filter.getKey()));
}
// Manage filter for custom type field
public <T> BooleanExpression manage(final Class<T> entityType, final PathBuilder<T> entityPath, final QueryDslFilter filter) {
BooleanExpression result;
// Create field path
final SimplePath<ExampleEnum> path = entityPath.getSimple(filter.getKey(), ExampleEnum.class);
// Parse value
final ExampleEnum value = ExampleEnum.valueOf(String.valueOf(filter.getValue()));
// Process operator
switch (filter.getOperator()) {
case IS_NULL_FUNCTION:
result = path.isNull();
break;
case NON_NULL_FUNCTION:
result = path.isNotNull();
break;
case EQUALS:
case EQUALS_FUNCTION:
case EQUALS_FUNCTION_EQ:
result = path.eq(value);
break;
case NOT_EQUALS:
case NON_EQUALS_FUNCTION:
case NON_EQUALS_FUNCTION_NE:
result = path.ne(value);
break;
default:
throw new QueryDslFiltersException(String.format("Operation '%s' not supported for type 'ExampleEnum'.", filter.getOperator()));
}
return result;
}
}
Also you may want to configure your custom date format to be managed at QueryDslDateTypeManager. To achieve this, you just need to call to DateTimeUtils.addDateTimeFormat or DateTimeUtils.addDateFormat to register your date format.
package io.github.squdan.querydsl.filters.examples;
import io.github.squdan.querydsl.filters.util.DateTimeUtils;
public class ConfigurationClass {
public void configure() {
DateTimeUtils.addDateTimeFormat(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
DateTimeUtils.addDateFormat(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
}
Once implemented your QueryDslTypeManager you must configure it into the repositories where you want to manage this types.
package io.github.squdan.querydsl.filters.examples;
import io.github.squdan.querydsl.filters.repository.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
public interface ExampleRepository extends JpaRepository<ExampleEntity, ID_TYPE>, QueryDslRepository<ExampleEntity, EntityPathBase<ExampleEntity>> {
// Required configuration
default Class<UserEntity> getEntityType() {
return ExampleEntity.class;
}
// Optional configuration
QueryDslExampleEnumTypeManager CUSTOM_TYPES = new QueryDslExampleEnumTypeManager();
default QueryDslExampleEnumTypeManager getCustomTypesManager() {
return CUSTOM_TYPES;
}
}
Now, you can use your custom filters in your application. These filters can be directly used in your java code or they can be received from out and mapping.
There are 2 kind of filters available to use (check at io.github.squdan.querydsl.filters.QueryDslOperators):
- Simple-Operators: format key{{operator}}value. Example: key=value
- Functions:
- Single parameter: format {{function_operator}}(key). Example: isNull(key)
- Double parameter: format {{function_operator}}(key, value). Example: equals(key, value)
Developing QueryDsl-Filters in java is quite easy. Example:
package io.github.squdan.querydsl.filters.examples;
import io.github.squdan.querydsl.filters.QueryDslFilter;
import io.github.squdan.querydsl.filters.QueryDslOperators;
import lombok.RequiredArgsConstructor;
import java.time.Instant;
import java.util.List;
@RequiredArgsConstructor
public class TestingFilters {
// Dependencies
private final ExampleRepository exampleRepository;
public void simpleFilters() {
// Is null
QueryDslFilter isNullFilter = new QueryDslFilter("test_field_string", QueryDslOperators.IS_NULL_FUNCTION);
// Not null
QueryDslFilter notNullFilter = new QueryDslFilter("test_field_string", QueryDslOperators.NON_NULL_FUNCTION);
// Equals
QueryDslFilter equalsFilter = new QueryDslFilter("test_field_string", QueryDslOperators.EQUALS, "test_value");
// Not equals
QueryDslFilter notEqualsFilter = new QueryDslFilter("test_field_string", QueryDslOperators.NOT_EQUALS, "test_value");
// Starts with
QueryDslFilter startsWithFilter = new QueryDslFilter("test_field_string", QueryDslOperators.STARTS_WITH_FUNCTION, "test_starts");
// Ends with
QueryDslFilter endsWithFilter = new QueryDslFilter("test_field_string", QueryDslOperators.ENDS_WITH_FUNCTION, "test_ends");
// Contains
QueryDslFilter containsFilter = new QueryDslFilter("test_field_string", QueryDslOperators.CONTAIN_FUNCTION, "test_contains");
// Greater than
QueryDslFilter greaterThanNumberFilter = new QueryDslFilter("test_field_decimal", QueryDslOperators.GREATER_THAN, "35.4");
QueryDslFilter greaterThanDateFilter = new QueryDslFilter("test_field_date", QueryDslOperators.GREATER_THAN, ADMIN.getCreatedOn().minus(1, ChronoUnit.DAYS));
// Greater than or equals
QueryDslFilter greaterThanOrEqualsNumberFilter = new QueryDslFilter("test_field_decimal", QueryDslOperators.GREATER_THAN_OR_EQUALS, "35.4");
QueryDslFilter greaterThanOrEqualsDateFilter = new QueryDslFilter("test_field_date", QueryDslOperators.GREATER_THAN_OR_EQUALS, ADMIN.getCreatedOn().minus(1, ChronoUnit.DAYS));
// Lower than
QueryDslFilter lowerThanNumberFilter = new QueryDslFilter("test_field_integer", QueryDslOperators.LOWER_THAN, "35");
QueryDslFilter lowerThanDateFilter = new QueryDslFilter("test_field_date", QueryDslOperators.LOWER_THAN, Instant.now());
// Lower than or equals
QueryDslFilter lowerThanOrEqualsNumberFilter = new QueryDslFilter("test_field_decimal", QueryDslOperators.LOWER_THAN_OR_EQUALS, "35.6");
QueryDslFilter lowerThanOrEqualsDateFilter = new QueryDslFilter("test_field_date", QueryDslOperators.LOWER_THAN_OR_EQUALS, Instant.now());
// Optional: pagination
Pageable pageable = PageRequest.of(0, 100, Sort.by(Sort.Direction.DESC, "test_field"));
// Execution
List<ExampleEntity> mayResults = exampleRepository.findAll(List.of(isNullFilter, equalsFilter), pageable);
}
public void nestedCollectionFilters() {
// Filter
QueryDslFilter isNullFilter = new QueryDslFilter("nested_collection.field_name", QueryDslOperators.IS_NULL_FUNCTION);
// Optional: pagination
Pageable pageable = PageRequest.of(0, 100, Sort.by(Sort.Direction.DESC, "test_field"));
// Execution
List<ExampleEntity> mayResults = exampleRepository.findAll(List.of(isNullFilter), pageable);
}
public void nestedObjectFiltersAtNestedCollection() {
// Filter
QueryDslFilter isNullFilter = new QueryDslFilter("nested_collection.nested_object.field_name", QueryDslOperators.IS_NULL_FUNCTION);
// Optional: pagination
Pageable pageable = PageRequest.of(0, 100, Sort.by(Sort.Direction.DESC, "field_name"));
// Execution
List<ExampleEntity> mayResults = exampleRepository.findAll(List.of(isNullFilter), pageable);
}
}
You may want to receive or configure dynamic filters as String, you can use QueryDslFiltersMapper to map this String to QueryDslFilter.
package io.github.squdan.querydsl.filters.examples;
import io.github.squdan.querydsl.filters.QueryDslFilter;
import io.github.squdan.querydsl.filters.QueryDslFiltersMapper;
import lombok.RequiredArgsConstructor;
import java.util.List;
@RestController
@RequestMapping(UserControllerInterface.BASE_PATH)
@RequiredArgsConstructor
public class ExampleController {
// Constants
public static final String API_PATH = "/api/1.0";
public static final String API_RESOURCE = "examples";
public static final String BASE_PATH = ApiConfiguration.API_PATH + "/" + API_RESOURCE;
public static final String ENDPOINT_FIND_BY = "";
// Dependencies
private final ExampleRepository exampleRepository;
@GetMapping(value = ExampleController.ENDPOINT_FIND_BY, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<UserDto>> findBy(
@Schema(
description = "Filters to apply into the search. Example: key=value, isNull(key)",
defaultValue = "key=value, contains(key, value)"
)
@RequestParam(value = ApiConfiguration.QUERY_FILTER_PARAM, required = false) final List<String> filters, final Pageable pageable
) {
// Map List<String> filters to List<QueryDslFilters>
final List<QueryDslFilter> queryDslFilters = QueryDslFiltersMapper.map(filters);
// Searching execution
final List<ExampleEntity> mayResults = exampleRepository.findAll(queryDslFilters, pageable);
//...
}
}
For bugs, questions and discussions please use the Github Issues.
- Email: i02torod@gmail.com