Skip to content

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 ​

InterfaceExpression TypeKey Functions
BooleanExpressionExtensionsBooleanExpressionand, or, andAnyOf, orAllOf, eq, nullif, coalesce
SimpleExpressionExtensionsSimpleExpression<T>eq, ne, in, notIn
ComparableExpressionExtensionsComparableExpression<T>gt, goe, lt, loe, between, nullif, coalesce, rangeTo
NumberExpressionExtensionsNumberExpression<T>gt, goe, lt, loe, between, nullif, coalesce, rangeTo
StringExpressionExtensionsStringExpressioncontains, startsWith, endsWith, like, matches, nullif, coalesce
TemporalExpressionExtensionsTemporalExpression<T>after, before
CollectionExpressionExtensionsCollectionExpressionBase<T, E>contains
SubQueryExtensionsEntityPath<T>exists, notExists

BooleanExpressionExtensions ​

Null-safe AND/OR combinators. The foundation for building dynamic WHERE clauses.

Functions ​

FunctionSignatureSQL
andBooleanExpression?.and(BooleanExpression?)a AND b
orBooleanExpression?.or(BooleanExpression?)a OR b
andAnyOfBooleanExpression?.andAnyOf(List<BooleanExpression?>)a AND (b OR c OR ...)
orAllOfBooleanExpression?.orAllOf(List<BooleanExpression?>)a OR (b AND c AND ...)
eqBooleanExpression?.eq(Boolean?)active = true
nullifBooleanExpression?.nullif(Boolean?)NULLIF(active, true)
coalesceBooleanExpression?.coalesce(Boolean?)COALESCE(active, false)

Examples ​

kotlin
// 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,
)
sql
-- 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 = ?)

Vararg overload v1.2.0+

andAnyOf and orAllOf also accept a vararg of BooleanExpression?. Call with dot notation since Kotlin disallows vararg on infix functions: predicate.andAnyOf(p1, p2, p3).


SimpleExpressionExtensions ​

Equality and membership operators for any expression type.

Functions ​

FunctionSignatureSQL
eqSimpleExpression<T>?.eq(T?)status = ?
eqSimpleExpression<T>?.eq(Expression<in T>?)status = default_status
eqSimpleExpression<T>?.eq(SubQueryExpression<T>?)price = (SELECT ...)
neSimpleExpression<T>?.ne(T?)status != ?
neSimpleExpression<T>?.ne(Expression<in T>?)status != default_status
inSimpleExpression<T>?.in(Collection<T>?)status IN (?, ?)
inSimpleExpression<T>?.in(SubQueryExpression<T>?)id IN (SELECT ...)
notInSimpleExpression<T>?.notIn(Collection<T>?)status NOT IN (?, ?)
notInSimpleExpression<T>?.notIn(SubQueryExpression<T>?)id NOT IN (SELECT ...)
inChunkedSimpleExpression<T>?.inChunked(Collection<T>?)col IN (?) OR col IN (?)
eqAllSimpleExpression<T>?.eqAll(CollectionExpression<*, in T>?) / eqAll(SubQueryExpression<T>?)col = ALL (...)
eqAnySimpleExpression<T>?.eqAny(CollectionExpression<*, in T>?) / eqAny(SubQueryExpression<T>?)col = ANY (...)
neAllSimpleExpression<T>?.neAll(CollectionExpression<*, in T>?)col <> ALL (...)
neAnySimpleExpression<T>?.neAny(CollectionExpression<*, in T>?)col <> ANY (...)

Examples ​

kotlin
// 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 size
sql
status = '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.

Subquery comparisons v1.2.0+

eq, in, and notIn also accept a SubQueryExpression<T>? for null-safe comparison against subqueries built with JPAExpressions.

kotlin
import com.querydsl.jpa.JPAExpressions

// price equals the max price across all products
val maxPriceSubQuery = JPAExpressions.select(product.price.max()).from(product)
selectFrom(product).where(product.price eq maxPriceSubQuery).fetch()

// category in subquery result
val cheapCategoriesSubQuery = JPAExpressions
    .selectDistinct(product.category)
    .from(product)
    .where(product.price.lt(10000))
selectFrom(product).where(product.category `in` cheapCategoriesSubQuery).fetch()

When the subquery argument is null, the predicate is skipped (returns null) so optional subquery filters can be plugged in without conditional builder code.


ComparableExpressionExtensions ​

Comparison and range operators for Comparable types (dates, strings, enums, etc.).

Functions ​

FunctionSignatureSQL
gtComparableExpression<T>?.gt(T?)col > ?
goeComparableExpression<T>?.goe(T?)col >= ?
ltComparableExpression<T>?.lt(T?)col < ?
loeComparableExpression<T>?.loe(T?)col <= ?
betweenComparableExpression<T>?.between(Pair<T?, T?>)col BETWEEN ? AND ?
betweenComparableExpression<T>?.between(ClosedRange<T>)col BETWEEN ? AND ?
notBetweenComparableExpression<T>?.notBetween(Pair<T?, T?>)col NOT BETWEEN ? AND ?
between (reverse)T?.between(Pair<ComparableExpression<T>?, ComparableExpression<T>?>)lower <= ? AND upper >= ?
nullifComparableExpression<T>?.nullif(T?)NULLIF(col, ?)
coalesceComparableExpression<T>?.coalesce(T?)COALESCE(col, ?)
rangeToComparableExpression<T>..ComparableExpression<T>(creates Pair for between)
gtAllComparableExpression<T>?.gtAll(CollectionExpression<*, in T>?) / gtAll(SubQueryExpression<T>?)col > ALL (...)
gtAnyComparableExpression<T>?.gtAny(CollectionExpression<*, in T>?) / gtAny(SubQueryExpression<T>?)col > ANY (...)
goeAllComparableExpression<T>?.goeAll(CollectionExpression<*, in T>?) / goeAll(SubQueryExpression<T>?)col >= ALL (...)
goeAnyComparableExpression<T>?.goeAny(CollectionExpression<*, in T>?) / goeAny(SubQueryExpression<T>?)col >= ANY (...)
ltAllComparableExpression<T>?.ltAll(CollectionExpression<*, in T>?) / ltAll(SubQueryExpression<T>?)col < ALL (...)
ltAnyComparableExpression<T>?.ltAny(CollectionExpression<*, in T>?) / ltAny(SubQueryExpression<T>?)col < ANY (...)
loeAllComparableExpression<T>?.loeAll(CollectionExpression<*, in T>?) / loeAll(SubQueryExpression<T>?)col <= ALL (...)
loeAnyComparableExpression<T>?.loeAny(CollectionExpression<*, in T>?) / loeAny(SubQueryExpression<T>?)col <= ANY (...)

All comparison functions also have Expression<T> overloads for comparing against other columns.

Examples ​

kotlin
// 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)
sql
-- 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 60

Pair-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:

kotlin
// 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:

kotlin
// 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 >= 50000

Checking if a point is within geo bounds:

kotlin
// 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 v1.2.0+

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.

kotlin
entity.price add 1000             // price + 1000
entity.price multiply taxRate     // price * tax_rate (column ref)
entity.total divide quantity      // total / quantity

Kotlin operator overloads v1.2.0+

Kotlin arithmetic operators (+, -, *, /, %, unary -) are also available for expression building (sort, projection, computed columns). These have a non-null contract (both sides required), distinct from the null-skip infix forms above.

kotlin
entity.price + 1000               // price + 1000
entity.price * taxRate            // price * tax_rate (column ref)
-entity.price                     // -price
(entity.price + entity.tax) * 2   // composes with parentheses

Use + / - / * / / / % when building expressions where both operands are guaranteed present. Use add / subtract / etc. when either side may be null and you want the whole expression skipped.

Why 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 ​

FunctionSignatureSQL
gtNumberExpression<T>?.gt(T?)col > ?
goeNumberExpression<T>?.goe(T?)col >= ?
ltNumberExpression<T>?.lt(T?)col < ?
loeNumberExpression<T>?.loe(T?)col <= ?
betweenNumberExpression<T>?.between(Pair<T?, T?>)col BETWEEN ? AND ?
betweenNumberExpression<T>?.between(ClosedRange<T>)col BETWEEN ? AND ?
notBetweenNumberExpression<T>?.notBetween(Pair<T?, T?>)col NOT BETWEEN ? AND ?
between (reverse)T?.between(Pair<NumberExpression<T>?, NumberExpression<T>?>)lower <= ? AND upper >= ?
nullifNumberExpression<T>?.nullif(T?)NULLIF(col, ?)
coalesceNumberExpression<T>?.coalesce(T?)COALESCE(col, ?)
rangeToNumberExpression<T>..NumberExpression<T>(creates Pair for between)
addNumberExpression<T>?.add(T?) / add(Expression<T>?)col + ?
subtractNumberExpression<T>?.subtract(T?) / subtract(Expression<T>?)col - ?
multiplyNumberExpression<T>?.multiply(T?) / multiply(Expression<T>?)col * ?
divideNumberExpression<T>?.divide(T?) / divide(Expression<T>?)col / ?
modNumberExpression<T>?.mod(T?) / mod(Expression<T>?)col % ?
+ / - / * / / / %operator NumberExpression<T>.plus/minus/times/div/rem(T | Expression<T>)col + ? (non-null contract)
unary -operator NumberExpression<T>.unaryMinus()-col
gtAllNumberExpression<T>?.gtAll(CollectionExpression<*, in T>?) / gtAll(SubQueryExpression<T>?)col > ALL (...)
gtAnyNumberExpression<T>?.gtAny(CollectionExpression<*, in T>?) / gtAny(SubQueryExpression<T>?)col > ANY (...)
goeAllNumberExpression<T>?.goeAll(CollectionExpression<*, in T>?)col >= ALL (...)
goeAnyNumberExpression<T>?.goeAny(CollectionExpression<*, in T>?)col >= ANY (...)
ltAllNumberExpression<T>?.ltAll(CollectionExpression<*, in T>?)col < ALL (...)
ltAnyNumberExpression<T>?.ltAny(CollectionExpression<*, in T>?)col < ANY (...)
loeAllNumberExpression<T>?.loeAll(CollectionExpression<*, in T>?)col <= ALL (...)
loeAnyNumberExpression<T>?.loeAny(CollectionExpression<*, in T>?)col <= ANY (...)

Examples ​

kotlin
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)
sql
price > 10000
price BETWEEN ? AND ?
score BETWEEN 0 AND 100
quantity <= ?

ALL/Any comparisons v1.2.0+

QueryDSL provides <op>All / <op>Any members for comparing against a collection or subquery. querydsl-ktx wraps them with the same null-skip semantics:

kotlin
import com.querydsl.jpa.JPAExpressions

// price greater than ALL prices in the Stationery category
val stationeryPrices = JPAExpressions.select(product.price).from(product)
    .where(product.category.eq("Stationery"))
selectFrom(product).where(product.price gtAll stationeryPrices).fetch()

// price equal to ANY price in cheap categories
val cheapPrices = JPAExpressions.select(product.price).from(product)
    .where(product.price.lt(10000))
selectFrom(product).where(product.price eqAny cheapPrices).fetch()

QueryDSL 5.1.0 has asymmetric coverage. The wrappers mirror this exactly:

FunctionCollectionExpressionSubQueryExpression
eqAll / eqAny (Simple, Comparable, Number)✓✓
neAll / neAny (Simple)✓✗ (member not provided)
gtAll / gtAny (Comparable, Number)✓✓
goeAll / ltAll / loeAll and *Any (Comparable)✓✓
goeAll / ltAll / loeAll and *Any (Number)✓✗ (member not provided on NumberExpression)

For Number columns that need the missing SubQuery variants, cast the path to a ComparableExpression view if the underlying type is Comparable<T>.


StringExpressionExtensions ​

Pattern matching and string comparison operators.

Functions ​

FunctionSignatureSQL
containsStringExpression?.contains(String?)LIKE '%val%'
containsIgnoreCaseStringExpression?.containsIgnoreCase(String?)LOWER(col) LIKE LOWER('%val%')
startsWithStringExpression?.startsWith(String?)LIKE 'val%'
startsWithIgnoreCaseStringExpression?.startsWithIgnoreCase(String?)LOWER(col) LIKE LOWER('val%')
endsWithStringExpression?.endsWith(String?)LIKE '%val'
endsWithIgnoreCaseStringExpression?.endsWithIgnoreCase(String?)LOWER(col) LIKE LOWER('%val')
equalsIgnoreCaseStringExpression?.equalsIgnoreCase(String?)LOWER(col) = LOWER(?)
notEqualsIgnoreCaseStringExpression?.notEqualsIgnoreCase(String?)LOWER(col) != LOWER(?)
likeStringExpression?.like(String?)LIKE ?
likeIgnoreCaseStringExpression?.likeIgnoreCase(String?)LOWER(col) LIKE LOWER(?)
notLikeStringExpression?.notLike(String?)NOT LIKE ?
matchesStringExpression?.matches(String?)REGEXP ?
containsStringExpression?.contains(Expression<String>?)LIKE '%' || other_col || '%'
startsWithStringExpression?.startsWith(Expression<String>?)LIKE other_col || '%'
endsWithStringExpression?.endsWith(Expression<String>?)LIKE '%' || other_col
nullifStringExpression?.nullif(Expression<String>?)NULLIF(col, other_col)
nullifStringExpression?.nullif(String?)NULLIF(col, ?)
coalesceStringExpression?.coalesce(Expression<String>?)COALESCE(col, other_col)
coalesceStringExpression?.coalesce(String?)COALESCE(col, ?)

contains, startsWith, and endsWith also have Expression<String> overloads.

Examples ​

kotlin
// 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(?)
sql
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(?)

Escape character for literal % and _ v1.2.0+

For patterns containing literal % or _ characters, chain escape '\' right after like, notLike, or likeIgnoreCase. This mirrors SQL's LIKE pattern ESCAPE 'char' syntax.

kotlin
name like "10\\%off" escape '\\'              // LIKE '10\%off' ESCAPE '\'
name notLike "10\\%off" escape '\\'           // NOT LIKE '10\%off' ESCAPE '\'
name likeIgnoreCase "10\\%OFF" escape '\\'    // LIKE '10\%OFF' ESCAPE '\' (case-insensitive)

escape is BooleanExpression?.escape(Char). Calling it on any expression that is not a like-family result throws ExpressionException.


TemporalExpressionExtensions ​

Temporal comparison operators for date/time expressions.

Functions ​

FunctionSignatureSQL
afterTemporalExpression<T>?.after(T?)col > ?
afterTemporalExpression<T>?.after(Expression<T>?)col > other_col
beforeTemporalExpression<T>?.before(T?)col < ?
beforeTemporalExpression<T>?.before(Expression<T>?)col < other_col

Examples ​

kotlin
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)
sql
created_at > '2024-01-01'
created_at < '2024-12-31'
end_date > start_date

after/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 ​

FunctionSignatureSQL
containsCollectionExpressionBase<T, E>?.contains(E?)? IN (col)
containsCollectionExpressionBase<T, E>?.contains(Expression<E>?)other_col IN (col)

Examples ​

kotlin
entity.roles contains "ADMIN"       // 'ADMIN' IN (roles)
entity.tags contains tag            // ? IN (tags), null-safe
sql
'ADMIN' IN (roles)

SubQueryExtensions ​

Shorthand EXISTS / NOT EXISTS sub-query builders.

Functions ​

FunctionSignatureSQL
existsEntityPath<T>.exists(vararg Predicate?)EXISTS (SELECT 1 FROM ...)
notExistsEntityPath<T>.notExists(vararg Predicate?)NOT EXISTS (SELECT 1 FROM ...)

Examples ​

kotlin
// 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)
sql
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) ​

kotlin
@Repository
class MyRepository : QuerydslRepository<MyEntity>() {
    // All 8 interfaces are available
}

Option 2: QuerydslSupport + selected interfaces ​

kotlin
@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 ​

kotlin
class PredicateBuilder : BooleanExpressionExtensions, SimpleExpressionExtensions {
    // Use extensions anywhere, not just in repositories
}

QuerydslRepository vs QuerydslSupport

QuerydslRepository<T>QuerydslSupport<T>
Extension interfacesAll 8 includedNone (add what you need)
domainClassAuto-resolved via genericsMust override manually
Use whenYou want everythingYou want minimal surface area