Extension Interfaces
querydsl-ktx provides 8 extension interfaces, each scoped to a specific QueryDSL expression type. All functions are null-safe: null arguments cause the condition to be skipped.
Overview
| Interface | Expression Type | Key Functions |
|---|---|---|
| BooleanExpressionExtensions | BooleanExpression | and, or, andAnyOf, orAllOf, eq, nullif, coalesce |
| SimpleExpressionExtensions | SimpleExpression<T> | eq, ne, in, notIn |
| ComparableExpressionExtensions | ComparableExpression<T> | gt, goe, lt, loe, between, nullif, coalesce, rangeTo |
| NumberExpressionExtensions | NumberExpression<T> | gt, goe, lt, loe, between, nullif, coalesce, rangeTo |
| StringExpressionExtensions | StringExpression | contains, startsWith, endsWith, like, matches, nullif, coalesce |
| TemporalExpressionExtensions | TemporalExpression<T> | after, before |
| CollectionExpressionExtensions | CollectionExpressionBase<T, E> | contains |
| SubQueryExtensions | EntityPath<T> | exists, notExists |
BooleanExpressionExtensions
Null-safe AND/OR combinators. The foundation for building dynamic WHERE clauses.
Functions
| Function | Signature | SQL |
|---|---|---|
and | BooleanExpression?.and(BooleanExpression?) | a AND b |
or | BooleanExpression?.or(BooleanExpression?) | a OR b |
andAnyOf | BooleanExpression?.andAnyOf(List<BooleanExpression?>) | a AND (b OR c OR ...) |
orAllOf | BooleanExpression?.orAllOf(List<BooleanExpression?>) | a OR (b AND c AND ...) |
eq | BooleanExpression?.eq(Boolean?) | active = true |
nullif | BooleanExpression?.nullif(Boolean?) | NULLIF(active, true) |
coalesce | BooleanExpression?.coalesce(Boolean?) | COALESCE(active, false) |
Examples
// AND: null side is ignored
val predicate = (entity.active eq true) and (entity.name eq name)
// OR group
val rolePredicate = (entity.role eq "ADMIN") or (entity.role eq "MANAGER")
// AND with OR subgroup
val complex = (entity.active eq true) andAnyOf listOf(
entity.role eq role,
entity.department eq department,
)-- AND (name = 'John')
active = true AND name = 'John'
-- AND (name = null) -> only left side
active = true
-- OR group
role = 'ADMIN' OR role = 'MANAGER'
-- AND with OR subgroup
active = true AND (role = ? OR department = ?)SimpleExpressionExtensions
Equality and membership operators for any expression type.
Functions
| Function | Signature | SQL |
|---|---|---|
eq | SimpleExpression<T>?.eq(T?) | status = ? |
eq | SimpleExpression<T>?.eq(Expression<in T>?) | status = default_status |
ne | SimpleExpression<T>?.ne(T?) | status != ? |
ne | SimpleExpression<T>?.ne(Expression<in T>?) | status != default_status |
in | SimpleExpression<T>?.in(Collection<T>?) | status IN (?, ?) |
notIn | SimpleExpression<T>?.notIn(Collection<T>?) | status NOT IN (?, ?) |
inChunked | SimpleExpression<T>?.inChunked(Collection<T>?) | col IN (?) OR col IN (?) |
Examples
// Equality
entity.status eq "ACTIVE" // status = 'ACTIVE'
entity.status eq null // null (skipped)
// Not equal
entity.status ne "DELETED" // status != 'DELETED'
// IN / NOT IN
entity.status `in` listOf("A", "B") // status IN ('A', 'B')
entity.status notIn listOf("C") // status NOT IN ('C')
entity.status `in` null // null (skipped)
// Large IN clause auto-split (default 1000 per chunk)
entity.id inChunked largeIdList // id IN (1..1000) OR id IN (1001..2000)
entity.id.inChunked(ids, 500) // custom chunk sizestatus = 'ACTIVE'
status != 'DELETED'
status IN ('A', 'B')
status NOT IN ('C')inChunked for Oracle
Oracle has a hard limit of 1000 items in a single IN clause. inChunked automatically splits large collections into multiple IN clauses joined with OR. The default chunk size is 1000, but you can customize it.
ComparableExpressionExtensions
Comparison and range operators for Comparable types (dates, strings, enums, etc.).
Functions
| Function | Signature | SQL |
|---|---|---|
gt | ComparableExpression<T>?.gt(T?) | col > ? |
goe | ComparableExpression<T>?.goe(T?) | col >= ? |
lt | ComparableExpression<T>?.lt(T?) | col < ? |
loe | ComparableExpression<T>?.loe(T?) | col <= ? |
between | ComparableExpression<T>?.between(Pair<T?, T?>) | col BETWEEN ? AND ? |
between | ComparableExpression<T>?.between(ClosedRange<T>) | col BETWEEN ? AND ? |
notBetween | ComparableExpression<T>?.notBetween(Pair<T?, T?>) | col NOT BETWEEN ? AND ? |
between (reverse) | T?.between(Pair<ComparableExpression<T>?, ComparableExpression<T>?>) | lower <= ? AND upper >= ? |
nullif | ComparableExpression<T>?.nullif(T?) | NULLIF(col, ?) |
coalesce | ComparableExpression<T>?.coalesce(T?) | COALESCE(col, ?) |
rangeTo | ComparableExpression<T>..ComparableExpression<T> | (creates Pair for between) |
All comparison functions also have Expression<T> overloads for comparing against other columns.
Examples
// Simple comparison
entity.date gt startDate // date > ?
entity.date goe startDate // date >= ?
entity.date lt endDate // date < ?
entity.date loe endDate // date <= ?
// BETWEEN with Pair: partial range support
entity.date between (from to to) // BETWEEN ? AND ?
entity.date between (from to null) // date >= ?
entity.date between (null to to) // date <= ?
entity.date between (null to null) // null (skipped)
// BETWEEN with ClosedRange
entity.age between (20..60) // BETWEEN 20 AND 60
// Reverse BETWEEN: value on left, expression bounds on right
now between (sale.startAt to sale.endAt)
// -> start_at <= now AND end_at >= now
// rangeTo operator (..): syntactic sugar for creating Pair
entity.date between (entity.startDate..entity.endDate)
// equivalent to: entity.date between (entity.startDate to entity.endDate)-- Full range
created_at BETWEEN '2024-01-01' AND '2024-12-31'
-- One-sided (from only)
created_at >= '2024-01-01'
-- One-sided (to only)
created_at <= '2024-12-31'
-- ClosedRange
age BETWEEN 20 AND 60Pair-based between for optional date ranges
The Pair overload is the most powerful feature for date range filters. A single expression handles all four combinations (both, from-only, to-only, neither) that would otherwise require a 4-branch if/else.
Reverse Between: Real-World Use Cases
The reverse between puts a value on the left and column bounds on the right. This pattern is surprisingly common:
Checking if a date falls within an active period:
// Is the coupon valid right now?
val now = LocalDateTime.now()
selectFrom(coupon)
.where(now between (coupon.validFrom to coupon.validUntil))
.fetch()
// SQL: valid_from <= '2025-04-10T12:00' AND valid_until >= '2025-04-10T12:00'Checking if a price falls within a discount range:
// Which discount tier applies to this order amount?
val orderAmount = 50000
selectFrom(discountTier)
.where(orderAmount between (discountTier.minAmount to discountTier.maxAmount))
.fetch()
// SQL: min_amount <= 50000 AND max_amount >= 50000Checking if a point is within geo bounds:
// Simplified bounding box check
selectFrom(store)
.where(
userLat between (store.southLat to store.northLat),
userLng between (store.westLng to store.eastLng),
)
.fetch()The reverse between is also null-safe: if the value is null, the entire expression returns null (skipped).
NumberExpressionExtensions
Same API as ComparableExpressionExtensions, but for NumberExpression.
Arithmetic operators
add, subtract, multiply, divide, mod are also available with the same null-safety contract: either side null returns null (the whole arithmetic expression is skipped). Each has a value overload and an Expression<T> overload.
entity.price add 1000 // price + 1000
entity.price multiply taxRate // price * tax_rate (column ref)
entity.total divide quantity // total / quantityWhy a separate interface?
In QueryDSL's type hierarchy, NumberExpression does not extend ComparableExpression. This is a QueryDSL design decision: NumberExpression inherits from ComparableExpressionBase while ComparableExpression is a separate branch. So querydsl-ktx provides a parallel set of operators specifically typed for NumberExpression.
Functions
| Function | Signature | SQL |
|---|---|---|
gt | NumberExpression<T>?.gt(T?) | col > ? |
goe | NumberExpression<T>?.goe(T?) | col >= ? |
lt | NumberExpression<T>?.lt(T?) | col < ? |
loe | NumberExpression<T>?.loe(T?) | col <= ? |
between | NumberExpression<T>?.between(Pair<T?, T?>) | col BETWEEN ? AND ? |
between | NumberExpression<T>?.between(ClosedRange<T>) | col BETWEEN ? AND ? |
notBetween | NumberExpression<T>?.notBetween(Pair<T?, T?>) | col NOT BETWEEN ? AND ? |
between (reverse) | T?.between(Pair<NumberExpression<T>?, NumberExpression<T>?>) | lower <= ? AND upper >= ? |
nullif | NumberExpression<T>?.nullif(T?) | NULLIF(col, ?) |
coalesce | NumberExpression<T>?.coalesce(T?) | COALESCE(col, ?) |
rangeTo | NumberExpression<T>..NumberExpression<T> | (creates Pair for between) |
Examples
entity.price gt 10000
entity.price between (minPrice to maxPrice)
entity.score between (0..100)
entity.quantity loe maxQuantity
// Reverse BETWEEN: value on left, expression bounds on right
orderAmount between (tier.minAmount to tier.maxAmount)
// -> min_amount <= orderAmount AND max_amount >= orderAmount
// rangeTo operator (..): syntactic sugar for creating Pair
orderAmount between (tier.minAmount..tier.maxAmount)price > 10000
price BETWEEN ? AND ?
score BETWEEN 0 AND 100
quantity <= ?StringExpressionExtensions
Pattern matching and string comparison operators.
Functions
| Function | Signature | SQL |
|---|---|---|
contains | StringExpression?.contains(String?) | LIKE '%val%' |
containsIgnoreCase | StringExpression?.containsIgnoreCase(String?) | LOWER(col) LIKE LOWER('%val%') |
startsWith | StringExpression?.startsWith(String?) | LIKE 'val%' |
startsWithIgnoreCase | StringExpression?.startsWithIgnoreCase(String?) | LOWER(col) LIKE LOWER('val%') |
endsWith | StringExpression?.endsWith(String?) | LIKE '%val' |
endsWithIgnoreCase | StringExpression?.endsWithIgnoreCase(String?) | LOWER(col) LIKE LOWER('%val') |
equalsIgnoreCase | StringExpression?.equalsIgnoreCase(String?) | LOWER(col) = LOWER(?) |
notEqualsIgnoreCase | StringExpression?.notEqualsIgnoreCase(String?) | LOWER(col) != LOWER(?) |
like | StringExpression?.like(String?) | LIKE ? |
likeIgnoreCase | StringExpression?.likeIgnoreCase(String?) | LOWER(col) LIKE LOWER(?) |
notLike | StringExpression?.notLike(String?) | NOT LIKE ? |
matches | StringExpression?.matches(String?) | REGEXP ? |
contains | StringExpression?.contains(Expression<String>?) | LIKE '%' || other_col || '%' |
startsWith | StringExpression?.startsWith(Expression<String>?) | LIKE other_col || '%' |
endsWith | StringExpression?.endsWith(Expression<String>?) | LIKE '%' || other_col |
nullif | StringExpression?.nullif(Expression<String>?) | NULLIF(col, other_col) |
nullif | StringExpression?.nullif(String?) | NULLIF(col, ?) |
coalesce | StringExpression?.coalesce(Expression<String>?) | COALESCE(col, other_col) |
coalesce | StringExpression?.coalesce(String?) | COALESCE(col, ?) |
contains, startsWith, and endsWith also have Expression<String> overloads.
Examples
// Substring search
entity.name contains keyword // name LIKE '%keyword%'
entity.name containsIgnoreCase keyword // case-insensitive
// Prefix / suffix
entity.name startsWith prefix // name LIKE 'prefix%'
entity.name endsWith suffix // name LIKE '%suffix'
// Pattern and regex
entity.name like "J%n" // name LIKE 'J%n'
entity.email matches "^[a-z]+@.*" // name REGEXP '^[a-z]+@.*'
// Case-insensitive equality
entity.email equalsIgnoreCase email // LOWER(email) = LOWER(?)name LIKE '%keyword%'
LOWER(name) LIKE LOWER('%keyword%')
name LIKE 'prefix%'
name LIKE '%suffix'
name LIKE 'J%n'
email REGEXP '^[a-z]+@.*'
LOWER(email) = LOWER(?)TemporalExpressionExtensions
Temporal comparison operators for date/time expressions.
Functions
| Function | Signature | SQL |
|---|---|---|
after | TemporalExpression<T>?.after(T?) | col > ? |
after | TemporalExpression<T>?.after(Expression<T>?) | col > other_col |
before | TemporalExpression<T>?.before(T?) | col < ? |
before | TemporalExpression<T>?.before(Expression<T>?) | col < other_col |
Examples
entity.createdAt after startDate // created_at > ?
entity.createdAt before endDate // created_at < ?
// Column comparison
entity.endDate after entity.startDate // end_date > start_date
// Null-safe
entity.createdAt after null // null (skipped)created_at > '2024-01-01'
created_at < '2024-12-31'
end_date > start_dateafter/before vs gt/goe/lt/loe
Use after/before for TemporalExpression (dates, timestamps) and gt/goe/lt/loe for ComparableExpression or NumberExpression. They generate the same SQL but are defined on different QueryDSL types.
CollectionExpressionExtensions
Membership check for mapped collection fields (e.g., @ElementCollection, @ManyToMany).
Functions
| Function | Signature | SQL |
|---|---|---|
contains | CollectionExpressionBase<T, E>?.contains(E?) | ? IN (col) |
contains | CollectionExpressionBase<T, E>?.contains(Expression<E>?) | other_col IN (col) |
Examples
entity.roles contains "ADMIN" // 'ADMIN' IN (roles)
entity.tags contains tag // ? IN (tags), null-safe'ADMIN' IN (roles)SubQueryExtensions
Shorthand EXISTS / NOT EXISTS sub-query builders.
Functions
| Function | Signature | SQL |
|---|---|---|
exists | EntityPath<T>.exists(vararg Predicate?) | EXISTS (SELECT 1 FROM ...) |
notExists | EntityPath<T>.notExists(vararg Predicate?) | NOT EXISTS (SELECT 1 FROM ...) |
Examples
// Before: verbose sub-query
JPAExpressions.selectOne()
.from(orderItem)
.where(orderItem.orderId.eq(order.id))
.exists()
// After: concise
orderItem.exists(orderItem.orderId eq order.id)
// NOT EXISTS
orderItem.notExists(orderItem.orderId eq order.id)EXISTS (SELECT 1 FROM order_item WHERE order_item.order_id = order.id)
NOT EXISTS (SELECT 1 FROM order_item WHERE order_item.order_id = order.id)Null predicates
Null predicates in the vararg are silently filtered out.
Selective Implementation
You don't have to use all 8 interfaces. Choose what you need:
Option 1: QuerydslRepository (all included)
@Repository
class MyRepository : QuerydslRepository<MyEntity>() {
// All 8 interfaces are available
}Option 2: QuerydslSupport + selected interfaces
@Repository
class MyRepository : QuerydslSupport<MyEntity>(),
SimpleExpressionExtensions,
StringExpressionExtensions {
override val domainClass = MyEntity::class.java
// Only eq, ne, in, notIn, contains, startsWith, etc.
// No number/comparable/temporal/collection extensions
}Option 3: Implement on any class
class PredicateBuilder : BooleanExpressionExtensions, SimpleExpressionExtensions {
// Use extensions anywhere, not just in repositories
}QuerydslRepository vs QuerydslSupport
QuerydslRepository<T> | QuerydslSupport<T> | |
|---|---|---|
| Extension interfaces | All 8 included | None (add what you need) |
domainClass | Auto-resolved via generics | Must override manually |
| Use when | You want everything | You want minimal surface area |