def book = Book.findByTitle("Groovy in Action")
book
.addToAuthors(name:"Dierk Koenig")
.addToAuthors(name:"Guillaume LaForge")
.save()
7 Object Relational Mapping (GORM)
Version: 8.0.0-SNAPSHOT
7 Object Relational Mapping (GORM)
Domain classes are core to any business application. They hold state about business processes and hopefully also implement behavior. They are linked together through relationships; one-to-one, one-to-many, or many-to-many.
GORM is Grails' object relational mapping (ORM) implementation. Under the hood it uses Hibernate (a very popular and flexible open source ORM solution) and thanks to the dynamic nature of Groovy with its static and dynamic typing, along with the convention of Grails, there is far less configuration involved in creating Grails domain classes.
Hibernate Versions
GORM for Hibernate is available against two Hibernate ORM versions:
-
Hibernate 5.6 is the default. The standard
grails-bomandgrails-hibernate5dependencies provide the long-standing, battle-tested integration, and existing applications continue to work without changes. -
Hibernate 7.4 is the future. Hibernate 7 is the actively developed line of Hibernate ORM and the recommended target for new applications. Grails provides a drop-in
grails-hibernate7integration (withgrails-hibernate7-bom), so most applications can switch with only a build-file change.
| Hibernate 5.6 remains the default so that existing applications keep working out of the box. Hibernate 7.4 is the strategic direction: new applications are encouraged to adopt it, and existing applications should plan to migrate. |
To switch an application to Hibernate 7 - and for the full list of Hibernate 5 to Hibernate 7 breaking changes and their workarounds - see the Hibernate 5 to Hibernate 7 Migration guide.
You can also write Grails domain classes in Java. See the section on Hibernate Integration for how to write domain classes in Java but still use dynamic persistent methods. Below is a preview of GORM in action:
7.1 Quick Start Guide
A domain class can be created with the create-domain-class command:
grails create-domain-class myapp.Person
| If no package is specified with the create-domain-class script, Grails automatically uses the application name as the package name. |
This will create a class at the location grails-app/domain/myapp/Person.groovy such as the one below:
package myapp
class Person {
}
If you have the dbCreate property set to "update", "create" or "create-drop" on your DataSource, Grails will automatically generate/modify the database tables for you.
|
You can customize the class by adding properties:
class Person {
String name
Integer age
Date lastVisit
}
grails console
This loads an interactive GUI where you can run Groovy commands with access to the Spring ApplicationContext, GORM, etc.
7.1.1 Basic CRUD
Try performing some basic CRUD (Create/Read/Update/Delete) operations.
Create
To create a domain class use Map constructor to set its properties and call save:
def p = new Person(
name: "Fred",
age: 40,
lastVisit: new Date(),
)
p.save()
The save method will persist your class to the database using the underlying Hibernate ORM layer.
List
To retrieve multiple instances, use the list method:
def people = Person.list()
This returns all Person records from the database.
You can also pass pagination and sorting parameters:
def fetchParams = [sort: 'name', order: 'asc', max: 10, offset: 0]
def people = Person.list(fetchParams)
The list method supports:
-
max– maximum number of results -
offset– starting position -
sort– property to sort by -
order– sort direction (ascordesc)
In addition to these you can specify advanced parameters such as cache, fetch, lock, readOnly, fetchSize, timeout, flushMode, and ignoreCase.
See list for the complete list of supported parameters.
Read
Grails transparently adds an implicit id property to your domain class which you can use for retrieval:
def p = Person.get(1)
assert p.id == 1
This uses the get method that expects a database identifier to read the Person object back from the database.
You can also load an object in a read-only state by using the read method:
def p = Person.read(1)
In this case the underlying Hibernate engine will not do any dirty checking and the object will not be persisted. Note that if you explicitly call the save method then the object is placed back into a read-write state.
In addition, you can also load a proxy for an instance by using the load method:
def p = Person.load(1)
This incurs no database access until a method other than getId() is called. Hibernate then initializes the proxied instance, or throws an exception if no record is found for the specified id.
Update
To update an instance, change some properties and then call save again:
def p = Person.get(1)
p.name = "Bob"
p.save()
You can also update multiple properties at once using the properties assignment:
def p = Person.get(1)
p.properties = [name: "Bob", age: 45]
p.save()
This will bind the given map to the domain instance, updating all matching properties in a single step.
Only properties defined in the domain class will be assigned, and any missing properties in the map will remain unchanged.
Delete
To delete an instance use the delete method:
def p = Person.get(1)
p.delete()
If a delete operation fails (for example due to database constraints), an exception is thrown.
You can handle this using a try/catch block:
def p = Person.get(1)
try {
p.delete(flush: true)
} catch (Exception e) {
println "Delete failed: ${e.message}"
}
Unlike the save method, the delete method does not support a failOnError parameter. Instead, errors are propagated as exceptions.
Using flush: true ensures the delete is executed immediately, so any errors are raised at that point.
Deleting Every Instance
deleteAll() deletes every persisted instance of a domain class and returns the number of rows removed:
Number removed = Person.deleteAll()
An argument map may be supplied. It controls how the delete is executed — it does not narrow what is
deleted. The supported argument is flush:
Person.deleteAll(flush: true)
deleteAll() and deleteAll(Map) remove every row for the domain class. There is no argument that
turns them into a partial delete.
|
To delete a subset, build a query and delete through it:
Person.where { age < 18 }.deleteAll()
The other deleteAll overloads delete only the instances you hand them:
def (fred, bob) = [Person.get(1), Person.get(2)]
Person.deleteAll(fred, bob) // varargs
Person.deleteAll([fred, bob]) // any Iterable
Person.deleteAll([flush: true], fred, bob)
Querying
To dynamically build queries based on optional parameters a common pattern is to use DetachedCriteria and progressively compose filters depending on the provided inputs.
Properties
Consider the following example using the Person domain class:
import grails.gorm.DetachedCriteria
private DetachedCriteria<Person> buildQuery(Map filterParams) {
def query = Person.where {}
if (filterParams.containsKey('id')) {
query = query.where { id == filterParams.id }
}
if (filterParams.containsKey('name')) {
query = query.where { name == filterParams.name }
}
if (filterParams.containsKey('age')) {
query = query.where { age == filterParams.age }
}
return query
}
Associations
You can filter by associated properties using dot notation or nested criteria. For example, if Person has a self-referencing relationship parent, you can filter by properties of the parent.
class Person {
String name
Integer age
Date lastVisit
Person parent
}
Using dot notation:
if (filterParams.containsKey('parent.name')) {
query = query.where { parent.name == filterParams.'parent.name' }
}
Using a nested criteria block, which is useful when filtering multiple properties of the association:
if (filterParams.containsKey('parent.name')) {
query = query.where {
parent {
name == filterParams.'parent.name'
}
}
}
This approach allows you to:
-
Build queries incrementally
-
Apply only the filters that are actually provided
-
Keep query logic reusable and centralized
You can then use this query in different ways.
Find a Single Result
def filterParams = [id: 1]
def person = buildQuery(filterParams).get()
Note: DetachedCriteria.get() returns a single result from the criteria query, while Person.get(id) is a static lookup by primary key on the domain class.
|
List Results
def filterParams = [age: 40]
def fetchParams = [sort: 'name', order: 'asc']
def people = buildQuery(filterParams).list(fetchParams)
Filters can be combined simply by passing multiple items:
def filterParams = [name: "Fred", age: 40]
def results = buildQuery(filterParams).list()
Count Results
def filterParams = [age: 40]
def total = buildQuery(filterParams).count()
Each condition is applied only if the corresponding parameter exists, making this pattern highly flexible for search forms and APIs.
Notes
-
Each call to
where {}returns a newDetachedCriteria, allowing safe chaining. -
This pattern avoids large, hardcoded query methods.
-
It works seamlessly with GORM and Hibernate in Grails.
This technique is especially useful in service layers where filtering logic must remain dynamic and maintainable.
7.2 Multi-Tenancy
A multi-tenant application serves several tenants from one deployment. GORM resolves the tenant for each operation from the tenant bound to the current thread, so the entry points below are how application code says "run this work as tenant X".
Running Work As a Tenant
grails.gorm.multitenancy.Tenants is the entry point.
withId
Tenants.withId(tenantId) { … } runs the closure as the given tenant and manages the session for it: a
session is opened for the scope of the call and closed on exit. In DATABASE mode that is also what selects
the tenant’s connection, so this is the method to reach for in almost all application code.
import grails.gorm.multitenancy.Tenants
Tenants.withId('acme') {
new Person(name: 'Fred').save()
Person.count()
}
The closure may take up to two parameters — the tenant id and the session:
Tenants.withId('acme') { String tenantId -> Person.count() }
Tenants.withId('acme') { String tenantId, session -> Person.count() }
An overload selects which datastore the tenant applies to by the domain class that maps to it, useful when an application has more than one datastore:
Tenants.withId(Person, 'acme') { Person.count() }
withTenant
Tenants.withTenant(tenantId) { … } binds the tenant to the current thread and nothing more. It opens no
session and manages no connection — the closure runs in whatever session the caller has already established,
and the caller remains responsible for opening and closing it.
Person.withNewSession {
Tenants.withTenant('acme') {
Person.count()
}
}
Reach for withTenant when a session already exists and you only need to change which tenant the enclosed
work resolves to; reach for withId when the work needs a session of its own. As with withId, a
withTenant(Class, tenantId, closure) overload picks the datastore by domain class.
Other Entry Points
| Method | Description |
|---|---|
|
The tenant id currently in effect, resolved from the bound tenant or, failing that, the configured tenant resolver. Throws if none can be found. |
|
Runs the closure under the tenant |
|
Runs the closure with no tenant. In |
|
Runs the closure once per tenant. |
Reading the Bound Tenant
grails.gorm.multitenancy.CurrentTenantHolder holds the tenant bound to the current thread. A thread can
hold a different tenant for each datastore in play, so lookups are scoped by datastore:
import grails.gorm.multitenancy.CurrentTenantHolder
Serializable tenantId = CurrentTenantHolder.get(datastore)
get() without a datastore returns the bound tenant when the answer is unambiguous, null when no tenant is
bound, and throws TenantException when different datastores hold different tenants — returning an arbitrary
one of them is how cross-tenant reads and writes happen. Prefer get(Datastore).
The remaining methods bind and unbind:
CurrentTenantHolder.set(datastore, 'acme')
CurrentTenantHolder.remove(datastore)
CurrentTenantHolder.withTenant(datastore, 'acme') { /* bound for this closure only */ }
CurrentTenantHolder.withoutTenant(datastore) { /* no tenant for this closure only */ }
Bindings may also be made against a datastore type rather than an instance; a binding for a specific instance wins over one for its type.
Prefer Tenants in application code. CurrentTenantHolder is the lower-level thread binding that
Tenants is built on, and is useful mainly when integrating with code that manages sessions itself.
|
7.3 Further Reading on GORM
For more information on using GORM see the dedicated documentation for the GORM project.