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 = ?)

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
neSimpleExpression<T>?.ne(T?)status != ?
neSimpleExpression<T>?.ne(Expression<in T>?)status != default_status
inSimpleExpression<T>?.in(Collection<T>?)status IN (?, ?)
notInSimpleExpression<T>?.notIn(Collection<T>?)status NOT IN (?, ?)
inChunkedSimpleExpression<T>?.inChunked(Collection<T>?)col IN (?) OR col IN (?)

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.


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)

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

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

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)

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 <= ?

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(?)

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