Compose Grails GORM Queries with DetachedCriteria
Stop repeating the same GORM filters. Learn how DetachedCriteria lets you define query logic once, combine it safely, and execute it inside an active Grails session.
01 Jun 2026, 06:51 UTC

The Same Filter, Written Three Times
In a Grails service layer, it is easy to end up with the same domain filter copied across several methods. One method finds published books. Another finds published books by a specific author. A third finds published books updated after a certain date. If the meaning of "published" changes—say, moving from a status column to a date range—you must find and edit every withCriteria block by hand.
The useful takeaway is to treat the filter as an object you can define once, combine with other filters, and execute only when the full query is known. In GORM, that object is DetachedCriteria.
DetachedCriteria Separates Definition from Execution
A standard withCriteria block runs immediately inside a Hibernate session and disappears when the block ends. DetachedCriteria is a query definition that is not tied to a session when you create it. You can pass it between methods, combine it with logical operators, and attach it to a session later for execution.
Think of it as a prepared query plan: nothing hits the database until you call a method such as list() or add it to an active withCriteria block.
Composable Book Search in a Service
Assume a Book domain class with title, author, and status fields. Instead of one large search method with many optional arguments, build small criteria generators.
import grails.gorm.DetachedCriteria
class BookService {
DetachedCriteria publishedBooks() {
return new DetachedCriteria(Book).and {
eq('status', 'PUBLISHED')
}
}
DetachedCriteria byAuthor(String authorName) {
return new DetachedCriteria(Book).and {
ilike('author', "%${authorName}%")
}
}
List<Book> search(String authorName, boolean publishedOnly) {
DetachedCriteria query = new DetachedCriteria(Book)
if (publishedOnly) {
query = query.and(publishedBooks())
}
if (authorName) {
query = query.and(byAuthor(authorName))
}
return Book.withCriteria {
add(query)
}
}
}
Run this code inside a Grails service. The search method does not know how the "published" filter is implemented; it only knows that publishedBooks() returns a composable criteria object. If the business rule changes, you update one helper method instead of every search path.
Verification
Create an integration test that saves three books: one published by Author A, one unpublished by Author A, and one published by Author B. Call search('Author A', true) and assert the result contains exactly one book. Then call search('Author A', false) and assert it returns two books. Run the test from the project root with:
./gradlew test --tests '*BookServiceSpec*'
If the test fails with a SessionException, the criteria was executed outside an active Hibernate session. Wrap the call in Book.withTransaction { ... } or ensure the service method runs inside a transaction.
Where DetachedCriteria Can Cause Problems
- Session binding: A
DetachedCriteriaobject is only a definition. Executing it outside a transaction orwithSessionblock throwsSessionExceptionbecause Hibernate has no connection to run the query against. - Lifecycle and memory: Do not store
DetachedCriteriainstances in static fields or HTTP session scope. They retain references to theSessionFactory, which can prevent garbage collection or break after application redeployment. - Lazy loading: Criteria that traverse associations or use projections may trigger lazy-loading exceptions if the session closes before the results are accessed. Fetch associations eagerly when needed.
Practical Check
After refactoring, search your codebase for static fields or session-scoped attributes that hold DetachedCriteria. If you find any, move the creation logic into a method or trait so each request builds a fresh instance.
Approach Comparison
| Approach | Best for | Trade-off |
|---|---|---|
Inline withCriteria | One-off queries | Simple but duplicated across services |
DetachedCriteria | Reusable service-layer filters | Requires active session; not serializable |
| HQL or SQL | Complex reporting queries | Maximum control, less type safety |
Next Step
Pick the three most repeated filters in your services. Move each into a method that returns DetachedCriteria, then compose them in your search methods. Run your integration suite to confirm the session boundary is respected.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.