Search DSL¶
Zygarde's type-safe search DSL provides a fluent, expressive way to query entities without dealing with JPA Criteria API complexity.
Overview¶
The search DSL automatically generates type-safe extension functions for entity fields, enabling intuitive query construction:
val books = bookDao.search {
title() contains "Kotlin"
author().country() eq "USA"
publishedYear() gte 2020
}
Basic Queries¶
Equality¶
// Find books with exact title
bookDao.search {
title() eq "Effective Kotlin"
}
// Not equal
bookDao.search {
status() notEq Status.ARCHIVED
}
Null Checks¶
// Find books with description
bookDao.search {
description() notNull()
}
// Find books without description
bookDao.search {
description() isNull()
}
Membership¶
// Find books with specific statuses
bookDao.search {
status() inList listOf(Status.PUBLISHED, Status.BESTSELLER)
}
// Exclude statuses
bookDao.search {
status() notInList listOf(Status.ARCHIVED, Status.DELETED)
}
String Operations¶
Contains¶
// Case-sensitive contains
bookDao.search {
title() contains "Kotlin"
}
// Case-insensitive contains
bookDao.search {
title() containsIgnoreCase "kotlin"
}
Starts With / Ends With¶
// Starts with
bookDao.search {
isbn() startsWith "978-"
}
// Ends with
bookDao.search {
title() endsWith "Guide"
}
Pattern Matching¶
// SQL LIKE pattern
bookDao.search {
title() like "%Kotlin%Programming%"
}
// NOT LIKE
bookDao.search {
title() notLike "%Beginner%"
}
Comparable Operations¶
For numeric, date, and other comparable types:
Comparisons¶
// Greater than
bookDao.search {
price() gt 50.0
}
// Greater than or equal
bookDao.search {
publishedYear() gte 2020
}
// Less than
bookDao.search {
pageCount() lt 300
}
// Less than or equal
bookDao.search {
rating() lte 3.5
}
Range Queries¶
// Between (inclusive)
bookDao.search {
publishedYear() between (2018 to 2023)
}
// Date ranges
bookDao.search {
createdAt() between (startDate to endDate)
}
Boolean Logic¶
AND Conditions¶
By default, all conditions are combined with AND:
bookDao.search {
title() contains "Kotlin"
status() eq Status.PUBLISHED
price() lte 50.0
}
// Equivalent to: title CONTAINS 'Kotlin' AND status = 'PUBLISHED' AND price <= 50.0
Explicit AND blocks:
OR Conditions¶
bookDao.search {
or {
title() contains "Kotlin"
title() contains "Java"
title() contains "Scala"
}
}
Combining AND/OR¶
bookDao.search {
status() eq Status.PUBLISHED
and {
or {
title() contains "Kotlin"
description() contains "Kotlin"
}
price() lte 50.0
}
}
// (status = 'PUBLISHED') AND ((title CONTAINS 'Kotlin' OR description CONTAINS 'Kotlin') AND price <= 50.0)
Negation¶
Relationship Traversal¶
Simple Relationships¶
Navigate relationships naturally:
// Find books by author country
bookDao.search {
author().country() eq "USA"
}
// Nested relationships
bookDao.search {
author().publisher().city() eq "New York"
}
Multiple Conditions on Relationships¶
bookDao.search {
author().country() eq "USA"
author().birthYear() gte 1970
author().status() eq AuthorStatus.ACTIVE
}
Collection Relationships¶
// Books with any review rating >= 4
bookDao.search {
reviews().rating() gte 4.0
}
// Books in specific categories
bookDao.search {
categories().name() eq "Programming"
}
Advanced Features¶
Range Overlap Detection¶
For temporal or numeric range queries:
import zygarde.core.dto.SearchRangeOverlap
// Find events overlapping with a date range
eventDao.search {
SearchRangeOverlap(
rangeStart = queryStartDate,
rangeEnd = queryEndDate,
targetStart = Event::startDate,
targetEnd = Event::endDate
).applyTo(this)
}
Date Range Queries¶
import zygarde.core.dto.SearchDateRange
import java.time.LocalDate
bookDao.search {
val dateRange = SearchDateRange(
start = LocalDate.of(2020, 1, 1),
end = LocalDate.of(2023, 12, 31)
)
createdAt() between (dateRange.start!! to dateRange.end!!)
}
Keyword Search¶
Multi-field keyword search:
import zygarde.core.dto.SearchKeyword
fun searchBooks(keyword: String?): List<Book> {
if (keyword.isNullOrBlank()) return bookDao.findAll()
return bookDao.search {
or {
title() containsIgnoreCase keyword
description() containsIgnoreCase keyword
author().name() containsIgnoreCase keyword
}
}
}
Pagination with Search DSL¶
Creating Specifications¶
Convert search DSL to Spring Data Specification:
import zygarde.data.jpa.search.EnhancedSearchImpl
val spec = EnhancedSearchImpl<Book>().apply {
title() contains "Kotlin"
status() eq Status.PUBLISHED
}.toSpecification()
val pageable = PageRequest.of(0, 20)
val page = bookDao.findAll(spec, pageable)
Reusable Specifications¶
object BookSpecs {
fun activeBooks(): Specification<Book> = EnhancedSearchImpl<Book>().apply {
status() eq Status.PUBLISHED
}.toSpecification()
fun byAuthorCountry(country: String): Specification<Book> =
EnhancedSearchImpl<Book>().apply {
author().country() eq country
}.toSpecification()
fun recentBooks(year: Int): Specification<Book> =
EnhancedSearchImpl<Book>().apply {
publishedYear() gte year
}.toSpecification()
}
// Combine specifications
val spec = BookSpecs.activeBooks()
.and(BookSpecs.byAuthorCountry("USA"))
.and(BookSpecs.recentBooks(2020))
val books = bookDao.findAll(spec)
Custom Actions¶
Extending the DSL¶
Create custom search actions for domain-specific operations:
import zygarde.data.jpa.search.ConditionAction
import javax.persistence.criteria.*
class CustomStringAction<ROOT, CURRENT>(
root: ROOT,
current: CURRENT,
path: Path<String>,
predicates: MutableList<Predicate>,
cb: CriteriaBuilder
) : ConditionAction<ROOT, CURRENT, String>(root, current, path, predicates, cb) {
infix fun matchesPattern(pattern: String): ROOT {
predicates.add(cb.like(path as Expression<String>, pattern))
return root
}
infix fun lengthGreaterThan(length: Int): ROOT {
predicates.add(cb.greaterThan(cb.length(path as Expression<String>), length))
return root
}
}
Using Custom Actions¶
// In your generated search DSL
fun SearchScope<Book, Book>.customTitle(): CustomStringAction<Book, Book, String> {
return CustomStringAction(
root = this as Book,
current = this,
path = /* field path */,
predicates = /* predicates list */,
cb = /* criteria builder */
)
}
// Usage
bookDao.search {
customTitle() matchesPattern "%[A-Z]%"
customTitle() lengthGreaterThan 10
}
Performance Considerations¶
Indexed Fields¶
Ensure fields used in queries are indexed:
@Entity
@Table(
name = "books",
indexes = [
Index(name = "idx_title", columnList = "title"),
Index(name = "idx_status", columnList = "status"),
Index(name = "idx_published_year", columnList = "published_year")
]
)
class Book(/* ... */) : AutoLongIdEntity()
Avoid N+1 Queries¶
Use fetch joins for relationships:
@Query("SELECT DISTINCT b FROM Book b JOIN FETCH b.author WHERE ...")
fun findWithAuthor(...): List<Book>
Or use @EntityGraph:
Limit Result Sets¶
Always paginate large result sets:
val spec = EnhancedSearchImpl<Book>().apply {
status() eq Status.PUBLISHED
}.toSpecification()
val pageable = PageRequest.of(0, 100) // Limit to 100 results
val page = bookDao.findAll(spec, pageable)
Common Patterns¶
Dynamic Filters¶
fun searchBooks(
title: String?,
authorId: Long?,
minPrice: Double?,
maxPrice: Double?,
status: Status?
): List<Book> = bookDao.search {
title?.let { title() containsIgnoreCase it }
authorId?.let { author().id() eq it }
minPrice?.let { price() gte it }
maxPrice?.let { price() lte it }
status?.let { status() eq it }
}
Sorting¶
val spec = EnhancedSearchImpl<Book>().apply {
status() eq Status.PUBLISHED
}.toSpecification()
val sort = Sort.by(
Sort.Order.desc("publishedYear"),
Sort.Order.asc("title")
)
val pageable = PageRequest.of(0, 20, sort)
val page = bookDao.findAll(spec, pageable)
Count Queries¶
val count = bookDao.count {
status() eq Status.PUBLISHED
publishedYear() gte 2020
}
println("Found $count published books since 2020")
Existence Checks¶
val hasKotlinBooks = bookDao.exists {
title() containsIgnoreCase "Kotlin"
status() eq Status.PUBLISHED
}
if (hasKotlinBooks) {
println("We have Kotlin books!")
}
Troubleshooting¶
Type Mismatch Errors¶
Problem: Compiler error on field comparisons
Solution: Ensure field types match comparison values:
Lazy Initialization Exceptions¶
Problem: LazyInitializationException when accessing relationships
Solution: Use fetch joins or @Transactional:
@Transactional(readOnly = true)
fun getBookWithAuthor(id: Long): Book {
val book = bookDao.findById(id).orElseThrow()
book.author.name // Access within transaction
return book
}
Query Performance Issues¶
Problem: Slow queries
Solutions: 1. Add database indexes on queried fields 2. Use projections instead of full entities 3. Implement pagination 4. Profile queries with show-sql: true
Next Steps¶
- JPA Extensions → - Advanced DAO features
- Model Mapping → - Convert entities to DTOs
- Common Patterns → - Best practices
- Tutorials → - Complete examples