Querydsl Predicate Builder is a powerful pattern for building dynamic, type-safe queries in Java applications. It provides a fluent API for constructing complex WHERE clauses programmatically, making it ideal for search filters, dynamic filtering, and complex query scenarios.
Overview and Setup
Dependencies
<!-- Querydsl JPA --> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>5.0.0</version> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>5.0.0</version> </dependency> <!-- For code generation --> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin>
Entity Classes
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String firstName; private String lastName; private String email; private Integer age; private Boolean active; private LocalDateTime createdAt; private LocalDateTime lastLogin; @Enumerated(EnumType.STRING) private UserRole role; @ManyToOne private Department department; @OneToMany(mappedBy = "user") private List<Order> orders; // constructors, getters, setters } @Entity public class Department { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String code; private Boolean active; @OneToMany(mappedBy = "department") private List<User> users; // constructors, getters, setters } @Entity public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String orderNumber; private BigDecimal amount; private LocalDateTime orderDate; private OrderStatus status; @ManyToOne private User user; // constructors, getters, setters } enum UserRole { ADMIN, MANAGER, USER, GUEST } enum OrderStatus { PENDING, PROCESSING, COMPLETED, CANCELLED } Basic Predicate Builder Implementation
Example 1: Generic Predicate Builder
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.*; import org.springframework.util.StringUtils; import java.time.LocalDateTime; import java.util.Collection; import java.util.function.Consumer; public class PredicateBuilder { private final BooleanBuilder booleanBuilder; public PredicateBuilder() { this.booleanBuilder = new BooleanBuilder(); } public static PredicateBuilder builder() { return new PredicateBuilder(); } // String operations public <T> PredicateBuilder and(StringExpression path, String value) { if (StringUtils.hasText(value)) { booleanBuilder.and(path.eq(value)); } return this; } public <T> PredicateBuilder andContains(StringExpression path, String value) { if (StringUtils.hasText(value)) { booleanBuilder.and(path.containsIgnoreCase(value)); } return this; } public <T> PredicateBuilder andStartsWith(StringExpression path, String value) { if (StringUtils.hasText(value)) { booleanBuilder.and(path.startsWithIgnoreCase(value)); } return this; } public <T> PredicateBuilder andEndsWith(StringExpression path, String value) { if (StringUtils.hasText(value)) { booleanBuilder.and(path.endsWithIgnoreCase(value)); } return this; } // Number operations public <T extends Number & Comparable<?>> PredicateBuilder and( NumberExpression<T> path, T value) { if (value != null) { booleanBuilder.and(path.eq(value)); } return this; } public <T extends Number & Comparable<?>> PredicateBuilder andGreaterThan( NumberExpression<T> path, T value) { if (value != null) { booleanBuilder.and(path.gt(value)); } return this; } public <T extends Number & Comparable<?>> PredicateBuilder andLessThan( NumberExpression<T> path, T value) { if (value != null) { booleanBuilder.and(path.lt(value)); } return this; } public <T extends Number & Comparable<?>> PredicateBuilder andBetween( NumberExpression<T> path, T from, T to) { if (from != null && to != null) { booleanBuilder.and(path.between(from, to)); } else if (from != null) { booleanBuilder.and(path.goe(from)); } else if (to != null) { booleanBuilder.and(path.loe(to)); } return this; } // Boolean operations public PredicateBuilder and(BooleanExpression path, Boolean value) { if (value != null) { booleanBuilder.and(path.eq(value)); } return this; } // Date operations public PredicateBuilder andAfter(DateTimeExpression<LocalDateTime> path, LocalDateTime value) { if (value != null) { booleanBuilder.and(path.after(value)); } return this; } public PredicateBuilder andBefore(DateTimeExpression<LocalDateTime> path, LocalDateTime value) { if (value != null) { booleanBuilder.and(path.before(value)); } return this; } public PredicateBuilder andBetween(DateTimeExpression<LocalDateTime> path, LocalDateTime from, LocalDateTime to) { if (from != null && to != null) { booleanBuilder.and(path.between(from, to)); } else if (from != null) { booleanBuilder.and(path.after(from)); } else if (to != null) { booleanBuilder.and(path.before(to)); } return this; } // Collection operations public <T> PredicateBuilder andIn(SimpleExpression<T> path, Collection<T> values) { if (values != null && !values.isEmpty()) { booleanBuilder.and(path.in(values)); } return this; } public <T> PredicateBuilder andNotIn(SimpleExpression<T> path, Collection<T> values) { if (values != null && !values.isEmpty()) { booleanBuilder.and(path.notIn(values)); } return this; } // Null checks public <T> PredicateBuilder andIsNull(SimpleExpression<T> path) { booleanBuilder.and(path.isNull()); return this; } public <T> PredicateBuilder andIsNotNull(SimpleExpression<T> path) { booleanBuilder.and(path.isNotNull()); return this; } // Custom predicate public PredicateBuilder and(Predicate predicate) { if (predicate != null) { booleanBuilder.and(predicate); } return this; } // Conditional building public PredicateBuilder andIf(boolean condition, Consumer<PredicateBuilder> consumer) { if (condition) { consumer.accept(this); } return this; } public Predicate build() { return booleanBuilder; } public BooleanBuilder getBooleanBuilder() { return booleanBuilder; } } Example 2: User-Specific Predicate Builder
import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; import java.time.LocalDateTime; import java.util.Collection; import static com.example.entity.QUser.user; public class UserPredicateBuilder { private final PredicateBuilder predicateBuilder; public UserPredicateBuilder() { this.predicateBuilder = PredicateBuilder.builder(); } public static UserPredicateBuilder builder() { return new UserPredicateBuilder(); } // User-specific methods public UserPredicateBuilder firstName(String firstName) { predicateBuilder.andContains(user.firstName, firstName); return this; } public UserPredicateBuilder lastName(String lastName) { predicateBuilder.andContains(user.lastName, lastName); return this; } public UserPredicateBuilder email(String email) { predicateBuilder.andContains(user.email, email); return this; } public UserPredicateBuilder age(Integer age) { predicateBuilder.and(user.age, age); return this; } public UserPredicateBuilder ageBetween(Integer minAge, Integer maxAge) { predicateBuilder.andBetween(user.age, minAge, maxAge); return this; } public UserPredicateBuilder ageGreaterThan(Integer minAge) { predicateBuilder.andGreaterThan(user.age, minAge); return this; } public UserPredicateBuilder ageLessThan(Integer maxAge) { predicateBuilder.andLessThan(user.age, maxAge); return this; } public UserPredicateBuilder active(Boolean active) { predicateBuilder.and(user.active, active); return this; } public UserPredicateBuilder role(UserRole role) { predicateBuilder.and(user.role, role); return this; } public UserPredicateBuilder roles(Collection<UserRole> roles) { predicateBuilder.andIn(user.role, roles); return this; } public UserPredicateBuilder createdAfter(LocalDateTime date) { predicateBuilder.andAfter(user.createdAt, date); return this; } public UserPredicateBuilder createdBefore(LocalDateTime date) { predicateBuilder.andBefore(user.createdAt, date); return this; } public UserPredicateBuilder createdBetween(LocalDateTime from, LocalDateTime to) { predicateBuilder.andBetween(user.createdAt, from, to); return this; } public UserPredicateBuilder lastLoginAfter(LocalDateTime date) { predicateBuilder.andAfter(user.lastLogin, date); return this; } public UserPredicateBuilder hasDepartment() { predicateBuilder.andIsNotNull(user.department); return this; } public UserPredicateBuilder departmentId(Long departmentId) { if (departmentId != null) { predicateBuilder.and(user.department.id, departmentId); } return this; } public UserPredicateBuilder departmentName(String departmentName) { predicateBuilder.andContains(user.department.name, departmentName); return this; } // Complex conditions public UserPredicateBuilder activeAdmins() { predicateBuilder.and(user.active.eq(true).and(user.role.eq(UserRole.ADMIN))); return this; } public UserPredicateBuilder inactiveUsers() { predicateBuilder.and(user.active.eq(false)); return this; } public UserPredicateBuilder recentlyActive(int days) { LocalDateTime cutoff = LocalDateTime.now().minusDays(days); predicateBuilder.and(user.lastLogin.after(cutoff)); return this; } // Custom predicate public UserPredicateBuilder withCustomPredicate(Predicate predicate) { predicateBuilder.and(predicate); return this; } public Predicate build() { return predicateBuilder.build(); } } Repository Usage Examples
Example 3: User Repository with Dynamic Queries
@Repository public class UserRepositoryCustomImpl implements UserRepositoryCustom { @PersistenceContext private EntityManager entityManager; private final JPAQueryFactory queryFactory; public UserRepositoryCustomImpl(EntityManager entityManager) { this.entityManager = entityManager; this.queryFactory = new JPAQueryFactory(entityManager); } @Override public List<User> findUsersByCriteria(UserSearchCriteria criteria) { return queryFactory .selectFrom(user) .where(buildUserPredicate(criteria)) .orderBy(getUserOrder(criteria)) .fetch(); } @Override public Page<User> findUsersByCriteria(UserSearchCriteria criteria, Pageable pageable) { JPAQuery<User> query = queryFactory .selectFrom(user) .where(buildUserPredicate(criteria)); long total = query.fetchCount(); List<User> content = query .orderBy(getUserOrder(criteria)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); return new PageImpl<>(content, pageable, total); } @Override public List<User> findActiveUsersWithRecentLogin(int days) { return queryFactory .selectFrom(user) .where(UserPredicateBuilder.builder() .active(true) .recentlyActive(days) .build()) .fetch(); } private Predicate buildUserPredicate(UserSearchCriteria criteria) { return UserPredicateBuilder.builder() .firstName(criteria.getFirstName()) .lastName(criteria.getLastName()) .email(criteria.getEmail()) .ageBetween(criteria.getMinAge(), criteria.getMaxAge()) .active(criteria.getActive()) .role(criteria.getRole()) .roles(criteria.getRoles()) .createdBetween(criteria.getCreatedFrom(), criteria.getCreatedTo()) .lastLoginAfter(criteria.getLastLoginAfter()) .departmentId(criteria.getDepartmentId()) .departmentName(criteria.getDepartmentName()) .build(); } private OrderSpecifier<?>[] getUserOrder(UserSearchCriteria criteria) { List<OrderSpecifier<?>> orders = new ArrayList<>(); if (criteria.getSortBy() != null) { switch (criteria.getSortBy()) { case "firstName" -> orders.add(getOrderSpecifier(user.firstName, criteria.getSortDirection())); case "lastName" -> orders.add(getOrderSpecifier(user.lastName, criteria.getSortDirection())); case "email" -> orders.add(getOrderSpecifier(user.email, criteria.getSortDirection())); case "age" -> orders.add(getOrderSpecifier(user.age, criteria.getSortDirection())); case "createdAt" -> orders.add(getOrderSpecifier(user.createdAt, criteria.getSortDirection())); default -> orders.add(user.createdAt.desc()); } } else { orders.add(user.createdAt.desc()); } return orders.toArray(new OrderSpecifier[0]); } private OrderSpecifier<?> getOrderSpecifier(Expression<?> expression, String direction) { return "desc".equalsIgnoreCase(direction) ? expression.desc() : expression.asc(); } } // Search criteria DTO public class UserSearchCriteria { private String firstName; private String lastName; private String email; private Integer minAge; private Integer maxAge; private Boolean active; private UserRole role; private List<UserRole> roles; private LocalDateTime createdFrom; private LocalDateTime createdTo; private LocalDateTime lastLoginAfter; private Long departmentId; private String departmentName; private String sortBy; private String sortDirection = "desc"; // constructors, getters, setters } Example 4: Complex Search with Joins
public class ComplexSearchPredicateBuilder { public static Predicate buildUserOrderSearchPredicate(UserOrderSearchCriteria criteria) { QUser user = QUser.user; QOrder order = QOrder.order; QDepartment department = QDepartment.department; BooleanBuilder predicate = new BooleanBuilder(); // User conditions if (StringUtils.hasText(criteria.getUserName())) { BooleanExpression namePredicate = user.firstName.containsIgnoreCase(criteria.getUserName()) .or(user.lastName.containsIgnoreCase(criteria.getUserName())); predicate.and(namePredicate); } if (criteria.getUserActive() != null) { predicate.and(user.active.eq(criteria.getUserActive())); } if (criteria.getUserRoles() != null && !criteria.getUserRoles().isEmpty()) { predicate.and(user.role.in(criteria.getUserRoles())); } // Department conditions if (criteria.getDepartmentId() != null) { predicate.and(user.department.id.eq(criteria.getDepartmentId())); } if (StringUtils.hasText(criteria.getDepartmentName())) { predicate.and(department.name.containsIgnoreCase(criteria.getDepartmentName())); } // Order conditions if (criteria.getMinOrderAmount() != null) { predicate.and(order.amount.goe(criteria.getMinOrderAmount())); } if (criteria.getMaxOrderAmount() != null) { predicate.and(order.amount.loe(criteria.getMaxOrderAmount())); } if (criteria.getOrderStatuses() != null && !criteria.getOrderStatuses().isEmpty()) { predicate.and(order.status.in(criteria.getOrderStatuses())); } if (criteria.getOrderDateFrom() != null && criteria.getOrderDateTo() != null) { predicate.and(order.orderDate.between(criteria.getOrderDateFrom(), criteria.getOrderDateTo())); } else if (criteria.getOrderDateFrom() != null) { predicate.and(order.orderDate.after(criteria.getOrderDateFrom())); } else if (criteria.getOrderDateTo() != null) { predicate.and(order.orderDate.before(criteria.getOrderDateTo())); } return predicate; } } // Service usage @Service @Transactional(readOnly = true) public class UserService { private final JPAQueryFactory queryFactory; private final UserRepository userRepository; public UserService(JPAQueryFactory queryFactory, UserRepository userRepository) { this.queryFactory = queryFactory; this.userRepository = userRepository; } public List<UserOrderDTO> searchUserOrders(UserOrderSearchCriteria criteria) { QUser user = QUser.user; QOrder order = QOrder.order; QDepartment department = QDepartment.department; return queryFactory .select(Projections.constructor(UserOrderDTO.class, user.id, user.firstName, user.lastName, user.email, department.name, order.orderNumber, order.amount, order.status, order.orderDate )) .from(user) .leftJoin(user.orders, order) .leftJoin(user.department, department) .where(ComplexSearchPredicateBuilder.buildUserOrderSearchPredicate(criteria)) .orderBy(user.lastName.asc(), order.orderDate.desc()) .fetch(); } public Map<String, Object> searchUsersWithStats(UserSearchCriteria criteria) { QUser user = QUser.user; Predicate predicate = UserPredicateBuilder.builder() .firstName(criteria.getFirstName()) .lastName(criteria.getLastName()) .email(criteria.getEmail()) .ageBetween(criteria.getMinAge(), criteria.getMaxAge()) .active(criteria.getActive()) .build(); // Get users List<User> users = queryFactory .selectFrom(user) .where(predicate) .orderBy(user.createdAt.desc()) .fetch(); // Get statistics NumberExpression<Integer> ageExpression = user.age; NumberExpression<BigDecimal> ageAvg = ageExpression.avg(); NumberExpression<Integer> ageMax = ageExpression.max(); NumberExpression<Integer> ageMin = ageExpression.min(); Tuple stats = queryFactory .select(ageAvg, ageMax, ageMin, user.count()) .from(user) .where(predicate) .fetchOne(); Map<String, Object> result = new HashMap<>(); result.put("users", users); result.put("totalUsers", stats.get(user.count())); result.put("averageAge", stats.get(ageAvg)); result.put("maxAge", stats.get(ageMax)); result.put("minAge", stats.get(ageMin)); return result; } } // DTO for complex results public class UserOrderDTO { private Long userId; private String firstName; private String lastName; private String email; private String departmentName; private String orderNumber; private BigDecimal orderAmount; private OrderStatus orderStatus; private LocalDateTime orderDate; // constructor, getters } Advanced Patterns
Example 5: Generic Repository with Predicate Builder
public interface GenericRepository<T, ID> { List<T> findAll(Predicate predicate); Page<T> findAll(Predicate predicate, Pageable pageable); Optional<T> findOne(Predicate predicate); long count(Predicate predicate); boolean exists(Predicate predicate); } @Repository public class GenericRepositoryImpl<T, ID> implements GenericRepository<T, ID> { @PersistenceContext private EntityManager entityManager; private final JPAQueryFactory queryFactory; private final Class<T> entityClass; public GenericRepositoryImpl(Class<T> entityClass, EntityManager entityManager) { this.entityManager = entityManager; this.queryFactory = new JPAQueryFactory(entityManager); this.entityClass = entityClass; } @Override public List<T> findAll(Predicate predicate) { PathBuilder<T> entityPath = new PathBuilder<>(entityClass, StringUtils.uncapitalize(entityClass.getSimpleName())); return queryFactory .selectFrom(entityPath) .where(predicate) .fetch(); } @Override public Page<T> findAll(Predicate predicate, Pageable pageable) { PathBuilder<T> entityPath = new PathBuilder<>(entityClass, StringUtils.uncapitalize(entityClass.getSimpleName())); JPAQuery<T> query = queryFactory .selectFrom(entityPath) .where(predicate); long total = query.fetchCount(); List<T> content = query .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); return new PageImpl<>(content, pageable, total); } @Override public Optional<T> findOne(Predicate predicate) { PathBuilder<T> entityPath = new PathBuilder<>(entityClass, StringUtils.uncapitalize(entityClass.getSimpleName())); T result = queryFactory .selectFrom(entityPath) .where(predicate) .fetchOne(); return Optional.ofNullable(result); } @Override public long count(Predicate predicate) { PathBuilder<T> entityPath = new PathBuilder<>(entityClass, StringUtils.uncapitalize(entityClass.getSimpleName())); return queryFactory .selectFrom(entityPath) .where(predicate) .fetchCount(); } @Override public boolean exists(Predicate predicate) { return count(predicate) > 0; } } Example 6: Dynamic Query Composition
@Service public class DynamicQueryService { private final JPAQueryFactory queryFactory; public DynamicQueryService(JPAQueryFactory queryFactory) { this.queryFactory = queryFactory; } public List<User> buildDynamicQuery(List<FilterCondition> conditions) { QUser user = QUser.user; PredicateBuilder predicateBuilder = PredicateBuilder.builder(); for (FilterCondition condition : conditions) { applyCondition(predicateBuilder, user, condition); } return queryFactory .selectFrom(user) .where(predicateBuilder.build()) .fetch(); } private void applyCondition(PredicateBuilder builder, QUser user, FilterCondition condition) { switch (condition.getField()) { case "firstName" -> builder.andContains(user.firstName, condition.getValue()); case "lastName" -> builder.andContains(user.lastName, condition.getValue()); case "email" -> builder.andContains(user.email, condition.getValue()); case "age" -> applyNumberCondition(builder, user.age, condition); case "active" -> builder.and(user.active, Boolean.valueOf(condition.getValue())); case "role" -> builder.and(user.role, UserRole.valueOf(condition.getValue())); case "createdAt" -> applyDateCondition(builder, user.createdAt, condition); default -> throw new IllegalArgumentException("Unknown field: " + condition.getField()); } } private void applyNumberCondition(PredicateBuilder builder, NumberExpression<Integer> path, FilterCondition condition) { Integer value = Integer.valueOf(condition.getValue()); switch (condition.getOperator()) { case "eq" -> builder.and(path, value); case "gt" -> builder.andGreaterThan(path, value); case "lt" -> builder.andLessThan(path, value); case "gte" -> builder.and(path.goe(value)); case "lte" -> builder.and(path.loe(value)); } } private void applyDateCondition(PredicateBuilder builder, DateTimeExpression<LocalDateTime> path, FilterCondition condition) { LocalDateTime value = LocalDateTime.parse(condition.getValue()); switch (condition.getOperator()) { case "after" -> builder.andAfter(path, value); case "before" -> builder.andBefore(path, value); case "eq" -> builder.and(path.eq(value)); } } } public class FilterCondition { private String field; private String operator; // eq, gt, lt, contains, etc. private String value; // constructors, getters, setters } Best Practices and Performance
1. Use Indexed Fields
// Good - uses indexed field predicateBuilder.and(user.email, email); // Avoid - non-indexed full text search unless necessary predicateBuilder.andContains(user.firstName, name);
2. Pagination for Large Results
public Page<User> searchUsersWithPagination(UserSearchCriteria criteria, Pageable pageable) { Predicate predicate = buildPredicate(criteria); // Always use pagination for large datasets return userRepository.findAll(predicate, pageable); } 3. Avoid N+1 Queries
// Use joins and projections to avoid N+1 List<UserDTO> users = queryFactory .select(Projections.constructor(UserDTO.class, user.id, user.firstName, user.lastName, department.name )) .from(user) .leftJoin(user.department, department) .where(predicate) .fetch();
4. Query Optimization
// Use exists instead of count for existence checks public boolean userExists(String email) { return queryFactory .selectOne() .from(user) .where(user.email.eq(email)) .fetchFirst() != null; } Testing Predicate Builders
@ExtendWith(MockitoExtension.class) class UserPredicateBuilderTest { @Test void shouldBuildPredicateWithFirstName() { // When Predicate predicate = UserPredicateBuilder.builder() .firstName("John") .build(); // Then assertThat(predicate).isNotNull(); } @Test void shouldBuildComplexPredicate() { // When Predicate predicate = UserPredicateBuilder.builder() .firstName("John") .lastName("Doe") .active(true) .ageBetween(25, 35) .role(UserRole.USER) .build(); // Then assertThat(predicate).isNotNull(); } @Test void shouldHandleNullValues() { // When Predicate predicate = UserPredicateBuilder.builder() .firstName(null) .lastName("") .age(null) .build(); // Then - should not throw exceptions and build valid predicate assertThat(predicate).isNotNull(); } } Conclusion
Querydsl Predicate Builder provides a robust, type-safe way to build dynamic queries in Java applications. Key benefits include:
- Type Safety: Compile-time checking of field names and types
- Fluent API: Readable and maintainable query construction
- Dynamic Queries: Easy building of complex, conditional queries
- Reusability: Predicate builders can be reused across different queries
- Testability: Easy to unit test query logic
Best practices for using Querydsl Predicate Builder:
- Keep predicate builders focused and single-purpose
- Use proper pagination for large datasets
- Optimize queries with proper indexing
- Test predicate builders thoroughly
- Consider performance implications of complex joins and conditions
This pattern is particularly useful for:
- Search and filter functionality
- Reporting and analytics
- Administrative interfaces
- API endpoints with complex filtering requirements
- Dynamic query generation based on user input