Eliminate 90% of BooleanBuilder boilerplate in QueryDSL dynamic queries with null-safe infix Kotlin extensions.
If you've used QueryDSL in Kotlin, you've written this pattern hundreds of times:
val builder = BooleanBuilder()
if (name != null) builder.and(member.name.contains(name))
if (status != null) builder.and(member.status.eq(status))
if (minAge != null && maxAge != null) builder.and(member.age.between(minAge, maxAge))
else if (minAge != null) builder.and(member.age.goe(minAge))
else if (maxAge != null) builder.and(member.age.loe(maxAge))
if (startDate != null && endDate != null) builder.and(member.createdAt.between(startDate, endDate))
else if (startDate != null) builder.and(member.createdAt.goe(startDate))
else if (endDate != null) builder.and(member.createdAt.loe(endDate))Every optional filter adds 1-3 lines. Range filters need 3 branches. The pattern is always the same.
selectFrom(member)
.where(
member.name contains name,
member.status eq status,
member.age between (minAge to maxAge),
member.createdAt between (startDate to endDate),
)
.page(pageable)Null parameters are automatically skipped. between with a Pair handles one-sided ranges.
30 lines to 10 lines.
// 1. Add the dependency
implementation("io.github.harryjhin:querydsl-ktx-spring-boot-starter:1.2.0")// 2. Extend QuerydslRepository
@Repository
class MemberRepository : QuerydslRepository<Member>() {
private val member = QMember.member
fun search(name: String?, status: String?, pageable: Pageable): Page<Member> =
selectFrom(member)
.where(
member.name contains name,
member.status eq status,
)
.page(pageable)
}| Approach | Drawback |
|---|---|
BooleanBuilder |
Verbose, error-prone with ranges |
Per-field helpers (statusEq()) |
Duplicated per entity, partial coverage |
Specification |
Separate from QueryDSL, no infix syntax |
| Top-level extensions | Global scope pollution, name clashes |
| querydsl-ktx | Standard, tested, complete |
Extensions are scoped through interface implementation -- no global namespace pollution. Learn more
- Null-safe dynamic queries --
entity.status eq nullreturnsnull(skipped), noifcheck needed. Replaces both BooleanBuilder and per-field helper functions. - One-sided range survival --
entity.date between (from to null)becomesdate >= from. A single expression handles 4 combinations that used to need 3-branchif/else. - Pagination without fetchResults() --
fetchResults()is deprecated since QueryDSL 5.0.page()auto-generates count queries for simple cases and accepts a lambda for complex ones.slice()andexactSlice()avoid count queries entirely. - Type-safe dynamic sorting --
SortSpecprovides a whitelist mapping forSortproperty names. No more?sort=password,ascsecurity holes or broken join-column sorting. - 8 extension interfaces -- null-safe infix operators for Boolean, Simple, Comparable, Number, String, Temporal, Collection, SubQuery expressions.
- Subquery / collection comparisons --
eqAll,gtAny,loeAlland other ALL/ANY variants with consistent null-skip semantics for bothCollectionExpressionandSubQueryExpressionarguments. - Reified expression templates --
numberTemplate<Float>(...)instead ofExpressions.numberTemplate(Float::class.java, ...). - Case/When DSL --
case<Int> { when(pred) then value; otherwise(default) }with null-safe branches. - Bulk DML --
modifying { }with auto flush/clear. - Multi-datasource ready -- auto-config selects the
@Primary EntityManagerFactorywhen multiple data sources are present. Non-primary factories require manualJPAQueryFactoryregistration. - GraalVM native image --
RuntimeHintsauto-registered via@ImportRuntimeHintsfor out-of-the-box native image support.
| Installation | Gradle, Maven setup and module selection |
| Quick Start | Write your first dynamic query in 5 minutes |
| User Guide | Dynamic queries, extensions, expressions, Case/When DSL, pagination, bulk DML |
| API Reference | Dokka-generated API documentation |
| Dependency | Version | Note |
|---|---|---|
| Spring Boot | 3.0+ | CI tests 3.0, 3.2, 3.3, 3.4. |
| QueryDSL | 5.1.0+ | |
| Kotlin | 1.7+ | |
| Java | 17+ |
This project provides an llms.txt for AI context via GitMCP.
claude mcp add querydsl-ktx -- mcp-remote https://gitmcp.io/HarryJhin/querydsl-ktxcodex mcp add querydsl-ktx -- mcp-remote https://gitmcp.io/HarryJhin/querydsl-ktxgemini mcp add querydsl-ktx -- mcp-remote https://gitmcp.io/HarryJhin/querydsl-ktx{
"mcpServers": {
"querydsl-ktx": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-remote@latest", "https://gitmcp.io/HarryJhin/querydsl-ktx/sse"]
}
}
}