1. Introduction

2. Introduction

GORM for Hibernate 7 (grails-data-hibernate7) is the Hibernate 7 persistence layer for GORM, the GRAILS Object Relational Mapping framework. It provides a high-level, convention-over-configuration API for mapping Groovy domain classes to a relational database via Hibernate ORM 7 and Jakarta EE 10.

2.1. Features

  • Convention-based ORM mapping — minimal configuration for common patterns

  • Full Hibernate 7 support with Jakarta EE 10 (jakarta.* packages)

  • Spring Boot 3.5 integration

  • Dynamic finders, named queries, where query DSL, HQL, and native SQL

  • Optimistic locking, second-level caching, and batch fetching

  • Comprehensive association mapping: one-to-one, one-to-many, many-to-many, basic collections

  • Multiple inheritance strategies: table-per-hierarchy, table-per-subclass, table-per-concrete-class

  • Multi-tenancy support

  • Groovy static mapping {} DSL for full control over table/column names, types, and strategies

2.2. Requirements

Component Version

JDK

17+

Groovy

4.0.x

Spring Boot

3.5.x

Hibernate ORM

7.x

Jakarta EE

10

2.3. Quick Start

Add the dependency to your Grails application and define a domain class:

class Book {
    String title
    String author
    Date dateCreated
    Date lastUpdated

    static constraints = {
        title blank: false
        author blank: false
    }
}

GORM automatically:

  • Creates a book table with id, version, title, author, date_created, and last_updated columns

  • Adds dynamic finders like Book.findByTitle('...'), Book.findAllByAuthor('...')

  • Injects save(), delete(), get(), list(), and other persistence methods

2.4. Release History

3. Release History

3.1. GORM for Hibernate 7 (Grails 8.x)

GORM for Hibernate 7 is a new integration module shipping with Grails 8.x that targets Hibernate ORM 7.x and Jakarta EE 10.

Key features and changes relative to GORM for Hibernate 5:

  • Hibernate ORM 7 — Full support for Hibernate 7, including Jakarta Persistence 3.2 and the Apache License 2.0.

  • Spring Boot 4 / Spring Framework 7 — Grails 8 vendors the removed org.springframework.orm.hibernate5 classes into grails-data-hibernate7-spring-orm under the org.grails.orm.hibernate.support.hibernate7 package.

  • HQL injection safety — Single-argument find, findAll, executeQuery, and executeUpdate accept a plain String as on Hibernate 5; when a Groovy GString is passed, its ${value} interpolations are bound as named parameters instead of being interpolated into the query text, so the idiomatic interpolated form is injection-safe by binding.

  • SQL query helpers - findWithSql / findAllWithSql are available with the same names as Hibernate 5.

  • Native query temporal types — Native SQL queries return java.time types by default instead of legacy java.sql types.

  • Removed deprecated Session API — Hibernate’s save(), update(), delete(), load(), and get() are replaced by JPA equivalents (persist, merge, remove, getReference, find). GORM’s own dynamic methods are unaffected.

  • CascadeType.SAVE_UPDATE removed — GORM’s ORM DSL cascade: 'save-update' string continues to work; direct use of the Hibernate CascadeType enum requires migration.

  • Removed annotations — @Where → @SQLRestriction, @Proxy removed, @LazyCollection removed.

  • DDL changes — char/Character maps to varchar(1); Oracle float/double map to IEEE float types; array columns use JSON/XML on some databases.

3.2. Previous History

For the history of GORM for Hibernate 5 (Grails 7.x and earlier), see the Grails 7 User Guide or the GORM for Hibernate 5 documentation.

3.3. Upgrade Notes

4. Upgrade Notes

4.1. Grails 8 / Hibernate 7

4.1.1. Query API — Injection Safety

The Grails 8 / Hibernate 7 query API keeps the Hibernate 5 calling conventions while making the idiomatic Groovy query form injection-safe by default. There is no breaking change here: the methods that accepted a plain String on Hibernate 5 still do.

Single-argument HQL overloads

The single-argument overloads continue to accept a plain String, exactly as on Hibernate 5:

  • find(CharSequence)

  • findAll(CharSequence)

  • executeQuery(CharSequence)

  • executeUpdate(CharSequence)

When a Groovy GString is passed instead, GORM extracts every ${value} interpolation and binds it as a named parameter rather than interpolating it into the query text, so the idiomatic interpolated form is injection-safe by binding:

// Plain String — executed as written (no params map required)
Book.findAll("from Book where active = true")

// GString — ${params.title} is bound as :p0, never interpolated (injection-safe)
Book.findAll("from Book where title = ${params.title}")

// Named parameters with a plain String
Book.findAll("from Book where title = :title", [title: params.title])

// Positional parameters
Book.executeQuery("from Book where title like ?1", [params.title + '%'])

As in any ORM, manually concatenating untrusted input into a plain String (for example "... where title = '" + userInput + "'") remains an injection risk and should be avoided in favour of the GString or parameterized forms above.

SQL query helpers
Book.findWithSql("select * from book where id = ${params.id}")
Book.findAllWithSql("select * from book where ...")

4.1.2. Schema-per-Tenant — Schema Names Are Now Quoted

DefaultSchemaHandler now quotes schema names using the JDBC identifier quote character (connection.metaData.identifierQuoteString) before executing SET SCHEMA and CREATE SCHEMA DDL statements. This prevents SQL injection via tenant identifiers.

Embedded quote characters are stripped from the schema name before quoting. If the JDBC driver does not support identifier quoting (returns " " or empty), the name is used unquoted as before — no behaviour change for such drivers.

No application changes are required unless your tenant resolver intentionally produces schema names containing the database’s identifier quote character (typically " or `` ` ``), in which case those characters will be stripped.

5. Getting Started

To use GORM 8.0.0 for Hibernate in Grails 8 you can specify the following configuration in build.gradle:

dependencies {
    implementation "org.grails.plugins:hibernate7:8.0.0"
    runtimeOnly 'org.hibernate:hibernate-ehcache:7.0.Final', {
    // exclude javax variant of hibernate-core
    exclude group: 'org.hibernate', module: 'hibernate-core'
    }
    runtimeOnly 'org.jboss.spec.javax.transaction:jboss-transaction-api_1.3_spec:2.0.0.Final', {
      // required for hibernate-ehcache to work with javax variant of hibernate-core excluded
    }
}

If you are using a version of Grails 3 earlier than 3.3 then you may need to enforce the GORM version. If you are using Grails 3.2.7 or above this can be done by modifying the gormVersion setting in gradle.properties:

gormVersion=8.0.0

Otherwise if you are using an earlier version of Grails you can force the GORM version by adding the following block directly above the dependencies block:

build.gradle
configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        if( details.requested.group == 'org.grails' &&
            details.requested.name.startsWith('grails-datastore')) {
            details.useVersion("8.0.0")
        }
    }
}
dependencies {
    ...
}

5.1. Common Problems

If you receive an error that indicates a failure to resolve the grails-datastore-simple dependency you may need to add the following to build.gradle directly above the dependencies block:

build.gradle
configurations.all {
    exclude module:'grails-datastore-simple'
}

5.2. Configuring Different Hibernate Versions

6. Hibernate Versions

GORM for Hibernate 7 (grails-data-hibernate7) requires Hibernate ORM 7.x and Jakarta EE 10 (jakarta.* packages).

6.1. Grails 8 with Hibernate 7

Grails 8 ships with Hibernate 5 as the default, but provides first-class support for Hibernate 7 via a dedicated BOM and plugin artifact.

build.gradle
dependencies {
    implementation enforcedPlatform("org.apache.grails:grails-hibernate7-bom:{version}")
    implementation "org.apache.grails:grails-hibernate7"
}

6.2. Hibernate 7.4 support line

Grails Hibernate 7 targets Hibernate ORM 7.4, the current stable Hibernate 7 line. Hibernate ORM 7.3 and 7.2 are limited-support lines, so new Hibernate 7 applications should validate against the Hibernate 7.4 behavior described below.

6.3. Migration checklist from Hibernate 5

If you are migrating an application from Hibernate 5 or the Grails Hibernate 5 plugin, review these remaining application-level changes. GORM compatibility issues fixed by this release, such as findWhere null handling and getAll ordering with converted ids, are documented in the relevant GORM API reference pages and do not require separate migration steps when using the fixed GORM APIs.

Area Hibernate 7.4 change Application migration action

Hibernate artifacts

Hibernate 7 applications must resolve the Hibernate 7 Grails plugin, BOM, and Hibernate ORM artifacts together.

Use grails-hibernate7-bom and grails-hibernate7 consistently. Do not mix Hibernate 5 and Hibernate 7 GORM or Hibernate artifacts in one runtime.

Jakarta Persistence

Hibernate 7 requires Jakarta Persistence 3.2 and Jakarta EE packages.

Replace any remaining javax.persistence.* or other javax.* imports with the matching jakarta.* APIs.

Direct Hibernate Session API

Session.save(), update(), delete(), and load() were removed. Session.get() remains available but is deprecated in favor of find().

GORM domain methods such as save(), delete(), get(), and load() are unaffected. Direct Session code inside withSession blocks should use persist(), merge(), remove(), and getReference() for removed methods, and prefer find() over deprecated direct Session.get() usage.

Hibernate annotations

Several Hibernate 5 annotations or enum constants were removed or renamed.

Replace direct uses of CascadeType.SAVE_UPDATE, @org.hibernate.annotations.Where, @Proxy, and @LazyCollection. The GORM ORM DSL cascade: 'save-update' string continues to work.

HQL string safety (no migration required)

Single-argument GORM HQL overloads accept a plain String exactly as on Hibernate 5. When a Groovy GString is passed, its interpolated values are bound as named parameters rather than interpolated into the query text, so the idiomatic form is injection-safe by binding.

No change is required. For dynamic values, prefer the GString form (auto-bound) or an explicit named/positional params map or list. See HQL Queries.

H5-specific cache setup

Hibernate 5 cache integrations and region-factory classes are not valid Hibernate 7 cache configuration.

Remove H5 cache dependencies and region-factory settings before booting on Hibernate 7. Reintroduce caching with a Hibernate 7 compatible provider such as JCache and test locked queries without relying on query cache entries.

Direct single-result queries

Hibernate 7.3 and later strictly throw when getSingleResult() or getSingleResultOrNull() sees more than one result.

GORM single-result helpers fixed by this release preserve GORM first-row behavior. Direct Hibernate or JPA queries should add setMaxResults(1), query for a list and choose the first row intentionally, or use a result-list transformer when duplicate collapse is required.

Read-only entity collections

Collections owned by entities loaded in read-only mode are now read-only too.

Do not mutate associations on read-only entities. Reload the entity in a writable session before changing its collections.

Timeout exceptions

Query or lock timeouts may now throw PersistenceException when the database marks the transaction for rollback.

Catch PersistenceException around direct Hibernate or JPA timeout-sensitive code and inspect the cause when you need to distinguish query timeout, lock timeout, and transaction rollback behavior.

Native SQL temporal values

Native SQL queries return java.time types instead of java.sql temporal types by default.

Update result handling to use java.time types, or set hibernate.query.native.prefer_jdbc_datetime_types=true during migration if legacy JDBC temporal values are required.

Direct aggregate HQL

Hibernate 7 validates projection result types strictly.

Use scalar-compatible return types for aggregate projections. For example, avg generally returns Double, count returns Long, and max or min follow the selected expression type.

Numeric expression typing

Hibernate 7 SQM typing is stricter about comparing unlike numeric expression types.

GORM where-query numeric comparison fixes are included in this release. In direct HQL, align domain numeric types or cast explicitly when comparing expressions of different numeric types.

Managed collection identity

Hibernate 7 detects duplicate managed collection wrappers more aggressively.

Prefer mutating managed collection instances with GORM addTo* and removeFrom* helpers instead of replacing collection objects. Be careful when merging detached graphs into an existing session.

Bidirectional associations

Hibernate 7 is less tolerant of inconsistent managed association state at flush time.

Keep both sides of bidirectional associations synchronized. Prefer GORM association helpers where possible.

Query locks and query cache

Locked queries are not query-cacheable.

Do not expect lock: true queries to use the query cache. Treat pessimistically locked queries as database reads that require fresh row state.

Programmatic datastore setup

Hibernate 7 test and programmatic datastore setups expose wrong package scanning more readily.

When constructing a datastore manually, scan the package that contains the domain and service classes, not only the caller or spec package.

Multiple datasources and OSIV

Hibernate 7 multiple-datasource and Open Session in View behavior should be validated explicitly for each application.

Run targeted migration tests for datasource switching, transaction routing, and GSP rendering under OSIV before migrating production applications with multiple datasources.

Views and scaffolding

Generated views and scaffolding can expose Hibernate 7 differences in association rendering and unique-constraint behavior.

Regression-test generated and custom association rendering, fields rendering, and unique constraints before enabling Hibernate 7 in applications that rely on scaffolding.

Version-column DDL

Hibernate 7.3 declares @Version columns not null by default.

Validate generated DDL against existing schemas before using dbCreate=update, especially for tables with legacy nullable version columns.

General DDL changes

Hibernate 7 changes generated DDL for several mappings, including char/Character, Oracle floating-point types, @ElementCollection sets, @CreationTimestamp, @UpdateTimestamp, and Oracle 23c LOB columns.

Prefer explicit database migrations for production schemas. Compare generated DDL in a staging database before using schema update tooling.

Schema actions and import.sql

Hibernate 7.3 and later run schema actions even when no entities are mapped.

Remove accidental import.sql files from the runtime classpath, or ensure schema-generation settings are intentional for persistence units with no mapped entities.

MySQL fetch depth

Hibernate 7.4 removes the MySQL dialect-specific default hibernate.max_fetch_depth=2.

Set hibernate.max_fetch_depth explicitly if the application relied on that implicit MySQL default.

Fetch joins with limits

Hibernate 7.4 applies pagination or limits with collection fetch joins in SQL instead of doing the limit in memory.

Review queries that combine max/offset or limits with collection fetch joins. Set the query hint org.hibernate.limitInMemory only when the previous in-memory behavior is required.

Oracle HQL date expressions

On Oracle, HQL current date and local date now translate to trunc(current_date).

Review Oracle queries that depended on a time component from those date expressions.

Eager @Any mappings

Hibernate 7.4 join-fetches eager @Any associations when loading an entity by id.

Review direct Hibernate @Any mappings for SQL shape and row-width changes.

Spanner PostgreSQL dialect

SpannerPostgreSQLDialect moved from hibernate-community-dialects to hibernate-core and package org.hibernate.dialect.

Update explicit dialect configuration to org.hibernate.dialect.SpannerPostgreSQLDialect, or rely on automatic dialect resolution.

Envers audited associations

Hibernate 7.3 respects RelationTargetAuditMode.NOT_AUDITED for audited associations.

If you use Envers, verify historic reads that previously expected the associated target entity to come from its audit table despite NOT_AUDITED.

6.4. Using GORM in Spring Boot

To use GORM for Hibernate in Spring Boot, add the necessary dependency to your Boot application:

build.gradle
implementation 'org.grails:gorm-hibernate7-spring-boot:8.0.0'

Then ensure you have configured a datasource and Hibernate as per the Spring Boot guide. For example in the case of MySQL:

application.yml
hibernate.hbm2ddl.auto: update
spring.datasource.url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
If you prefer to use the Grails way of configuring the DataSource (with dataSource.url etc.), these will work as well.
Application.groovy
import groovy.transform.CompileStatic
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration

@CompileStatic
@SpringBootApplication(exclude = HibernateJpaAutoConfiguration)
class Application {
    static void main(String[] args) {
        SpringApplication.run(Application, args)
    }
}
You need to exclude the HibernateJpaAutoconfiguration as we are using GORM. Using SpringBootApplication without a basePackages attribute results in Boot scanning for classes in the same package or any package nested within the Application class package. If your GORM entities are in a different package, specify the package name as the value of the basePackages attribute on the @SpringBootApplication annotation.

Finally create your GORM entities and ensure they are annotated with grails.persistence.Entity:

Person.groovy
import grails.persistence.Entity

@Entity
class Person {
    String firstName
    String lastName
}

Note that Spring Boot does not include any kind of OpenSessionInView interceptor so if you try and invoke GORM methods in a Spring @Controller you may encounter a session not found error. To eliminate this problem make sure your @Controller methods are annotated with @Transactional. For example:

PersonController.groovy
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class PersonController {

    @RequestMapping("/people")
    @Transactional(readOnly = true)
    public List<String> people() {
        Person.list().collect { Person p ->
            "$p.firstName $p.lastName".toString()
        }
    }
}

In addition, if you wish to return a GORM instance from a Spring @Controller, it should be noted that Spring uses Jackson for JSON marshalling, and Jackson will attempt to marshal the entire object to JSON, which can present an issue since GORM adds additional persistence related properties to your domain instance. To resolve this issue you should use @JsonIgnoreProperties on your GORM entity class to ignore any properties added by GORM:

Person.groovy
import grails.persistence.Entity
import com.fasterxml.jackson.annotation.JsonIgnoreProperties

@Entity
@JsonIgnoreProperties(['dirtyPropertyNames', 'errors', 'dirty', 'attached', 'version'])
class Person {
    String firstName
    String lastName
}

6.5. Using GORM for Hibernate Outside Grails

If you wish to use GORM for Hibernate outside a Grails application you should declare the necessary dependencies for GORM and the database you are using, for example in Gradle:

implementation platform("org.apache.grails:grails-bom:8.0.0")
implementation "org.apache.grails.data:grails-data-hibernate7-core"
runtimeOnly "com.h2database"
runtimeOnly "org.apache.tomcat:tomcat-jdbc"
runtimeOnly "org.slf4j:slf4j-nop"
The above example also uses the H2 Database and Tomcat connection pool. However, other pool implementations are supported including commons-dbcp, tomcat pool or hikari. If a connection pool is not specified org.springframework.jdbc.datasource.DriverManagerDataSource is used, which creates a new connection to the database each time you request a connection. The latter will probably cause issues with an H2 in-memory database in that it will create a new in-memory database each time a connection is requested, losing previously created tables. Normal databases (MySql, Postgres or even file-based H2) are not affected.

Then create your entities in the src/main/groovy directory and annotate them with the grails.gorm.annotation.Entity annotation:

import grails.gorm.annotation.Entity
import org.grails.datastore.gorm.GormEntity

@Entity
class Person implements GormEntity<Person> { (1)
    String firstName
    String lastName
    static constraints = {
        firstName blank:false
        lastName blank:false
    }
}
1 Use of GormEntity is merely to aid IDE support outside of Grails. When used inside a Grails context, some IDEs will use the grails-app/domain location as a hint to enable code completion.

Then you need to place the bootstrap logic somewhere in your application which uses HibernateDatastore:

import org.grails.orm.hibernate.HibernateDatastore
Map configuration = [
    'hibernate.hbm2ddl.auto':'create-drop',
    'dataSource.url':'jdbc:h2:mem:myDB'
]
HibernateDatastore datastore = new HibernateDatastore(configuration, Person)

For more information on how to configure GORM see the Configuration section.

7. Quick Start Guide

8. Quick Start Guide

This section covers getting up and running with GORM for Hibernate 7 quickly.

For full configuration options, see Configuration. For persistence basics, see Persistence Basics.

8.1. Basic CRUD

9. Basic CRUD

Every GORM domain class automatically gets Create, Read, Update, and Delete (CRUD) operations.

9.1. Create

// Constructor with named parameters
def book = new Book(title: 'Groovy in Action', author: 'Dierk König')
book.save()

// Or using the create() factory method
def book = Book.create(title: 'Grails in Action', author: 'Glen Smith')

9.2. Read

// By primary key
def book = Book.get(1)

// Returns null if not found
def book = Book.get(999)    // null

// Get multiple by IDs
def books = Book.getAll(1, 2, 3)

// The returned list preserves the supplied id order
def books = Book.getAll(3, 1, 2)

// Load a proxy (no immediate SELECT)
def book = Book.load(1)

// List all
def books = Book.list()

// With pagination
def books = Book.list(max: 10, offset: 0, sort: 'title', order: 'asc')

// Dynamic finders
def book  = Book.findByTitle('Groovy in Action')
def books = Book.findAllByAuthor('Dierk König')
def count = Book.countByAuthor('Dierk König')

// Property-map queries; null values match null properties
def unreleased = Book.findWhere(releaseDate: null)
def matching = Book.findAllWhere(author: 'Dierk König', releaseDate: null)

9.3. Update

def book = Book.get(1)
book.title = 'Updated Title'
book.save()

9.4. Delete

def book = Book.get(1)
book.delete()

9.5. Validation

save() runs constraints before persisting and returns null if validation fails:

def book = new Book(title: '')          // blank title violates constraint
if (!book.save()) {
    println book.errors.allErrors       // print validation errors
}

// Throw on failure instead
book.save(failOnError: true)            // throws ValidationException

9.6. Transactions

GORM operations run inside Hibernate sessions. Use withTransaction for explicit transaction control:

Book.withTransaction {
    def book = new Book(title: 'Tx Book', author: 'Author')
    book.save()
    // any exception here rolls back the transaction
}

10. Configuration

GORM for Hibernate can be configured with the grails-app/conf/application.yml file when using Grails, the src/main/resources/application.yml file when using Spring Boot or by passing a Map or instanceof the PropertyResolver interface to the org.grails.orm.hibernate.HibernateDatastore class when used standalone.

All configuration options are read and materialized into an instance of HibernateConnectionSourceSettings.

10.1. Configuration Example

If you are using Grails or Spring Boot, the following is an example of configuration specified in application.yml:

dataSource:
    pooled: true
    dbCreate: create-drop
    url: jdbc:h2:mem:devDb
    driverClassName: org.h2.Driver
    username: sa
    password:
hibernate:
    cache:
        queries: false
        use_second_level_cache: true
        use_query_cache: false
        region.factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory

Each one of the settings under the dataSource block is set on the DataSourceSettings property of HibernateConnectionSourceSettings.

Whilst each setting under the hibernate block is set on the HibernateSettings property.

10.2. Configuration Reference

You can refer to the HibernateConnectionSourceSettings class for all available configuration options, but below is a table of the common ones:

name description default value

grails.gorm.flushMode

The flush mode to use

COMMIT

grails.gorm.failOnError

Whether to throw an exception on validation error

false

grails.gorm.default.mapping

The default mapping to apply to all classes

null

grails.gorm.default.constraints

The default constraints to apply to all classes

null

grails.gorm.multiTenancy.mode

The multi tenancy mode

NONE

The following are common configuration options for the SQL connection:

name description default value

dataSource.url

The JDBC url

jdbc:h2:mem:grailsDB

dataSource.driverClassName

The class of the JDBC driver

detected from URL

dataSource.username

The JDBC username

null

dataSource.password

The JDBC password

null

dataSource.jndiName

The name of the JNDI resource for the DataSource

null

dataSource.pooled

Whether the connection is pooled

true

dataSource.lazy

Whether a LazyConnectionDataSourceProxy should be used

true

dataSource.transactionAware

Whether a TransactionAwareDataSourceProxy should be used

true

dataSource.readOnly

Whether the DataSource is read-only

false

dataSource.options

A map of options to pass to the underlying JDBC driver

null

And the following are common configuration options for Hibernate:

name description default value

hibernate.dialect

The hibernate dialect to use

detected automatically from DataSource

hibernate.readOnly

Whether Hibernate should be read-only

false

hibernate.configClass

The configuration class to use

HibernateMappingContextConfiguration

hibernate.hbm2ddl.auto

Whether to create the tables on startup

none

hibernate.use_second_level_cache

Whether to use the second level cache

true

hibernate.cache.queries

Whether to cache queries (see Caching Queries)

false

hibernate.cache.use_query_cache

Enables the query cache

false

hibernate.configLocations

Location of additional Hibernate XML configuration files

hibernate.packagesToScan

Specify packages to search for autodetection of your entity classes in the classpath

hibernate.grails.proxy.lazy_to_string

Whether toString() on an uninitialized proxy avoids initializing it (see Proxies and toString())

In addition, any additional settings that start with hibernate. are passed through to Hibernate, so if there is any specific feature of Hibernate you wish to configure that is possible.

10.2.1. Proxies and toString()

Grails proxies answer identifier access (id, getId(), ident()) and metaClass access without initializing the proxy. By default, calling toString() on an uninitialized proxy initializes it and delegates to the domain class’s own toString() implementation.

If you prefer toString() to also avoid initialization — for example to keep log statements from triggering database access — enable lazy toString() in application.yml:

hibernate:
    grails:
        proxy:
            lazy_to_string: true

With this setting enabled, toString() on an uninitialized proxy returns entityName:id (for example com.example.Author:1) without initializing it. Once the proxy has been initialized, toString() always delegates to the domain class’s implementation.

The above table covers the common configuration options. For all configuration refer to properties of the HibernateConnectionSourceSettings class.

10.3. The Default Mapping & Constraints

The grails.gorm.default.mapping and grails.gorm.default.constraints settings deserve special mention. These define the default ORM mapping and the default Validation Constraints used by each entity.

10.3.1. Altering the Default Database Mapping

You may have reason to want to change how all domain classes map to the database. For example, by default GORM uses the native id generation strategy of the database, whether that be an auto-increment column or a sequence.

If you wish to globally change all domain classes to use a uuid strategy then you can specify that in the default mapping:

grails-app/conf/application.groovy
grails.gorm.default.mapping = {
        cache true
        id generator:'uuid'
}

As you can see you can assign a closure that is equivalent to the mapping block used to customize how a domain class maps to a database table.

Because the setting is Groovy configuration it must go into a Groovy-aware configuration format. This can be grails-app/conf/application.groovy in Grails, or src/main/resources/application.groovy in Spring Boot.

10.3.2. Altering the Default Constraints

For validation, GORM applies a default set of constraints to all domain classes.

For example, by default all properties of GORM classes are not nullable by default. This means a value has to be supplied for each property, otherwise you will get a validation error.

In most cases this is what you want, but if you are dealing with a large number of columns, it may prove inconvinient.

You can alter the default constraints using Groovy configuration using the grails.gorm.default.constraints setting:

grails-app/conf/application.groovy
grails.gorm.default.constraints = {
    '*'(nullable: true, size: 1..20)
}

In the above example, all properties are allowed to be nullable by default, but limited to a size of between 1 and 20.

10.4. Hibernate Customization

If you want to hook into GORM and customize how Hibernate is configured there are a variety of ways to achieve that when using GORM.

Firstly, as mentioned previously, any configuration you specify when configuring GORM for Hibernate will be passed through to Hibernate so you can configure any setting of Hibernate itself.

For more advanced configuration you may want to configure or supply a new HibernateConnectionSourceFactory instance or a HibernateMappingContextConfiguration or both.

10.4.1. The HibernateConnectionSourceFactory

The HibernateConnectionSourceFactory is used to create a new Hibernate SessionFactory on startup.

If you are using Spring, it is registered as a Spring bean using the name hibernateConnectionSourceFactory and therefore can be overridden.

If you are not using Spring it can be passed to the constructor of the HibernateDatastore class on instantiation.

The HibernateConnectionSourceFactory has a few useful setters that allow you to specify a Hibernate Interceptor or MetadataContributor (Hibernate 5+ only).

10.4.2. The HibernateMappingContextConfiguration

HibernateMappingContextConfiguration is built by the HibernateConnectionSourceFactory, but a customized version can be specified using the hibernate.configClass setting in your configuration:

grails-app/conf/application.yml
hibernate:
        configClass: com.example.MyHibernateMappingContextConfiguration

The customized version should extend HibernateMappingContextConfiguration and using this class you can add additional classes, packages, hbm.cfg.xml files and so on.

11. Domain Modelling in GORM

12. Domain Classes

Domain classes are the heart of a GORM application. They represent the data model and are mapped to database tables automatically by convention.

12.1. Anatomy of a Domain Class

class Book {
    String title
    String author
    Integer pages
    Date dateCreated         (1)
    Date lastUpdated         (1)

    static constraints = {   (2)
        title  blank: false, maxSize: 255
        author blank: false
        pages  min: 1, nullable: true
    }

    static mapping = {       (3)
        table 'books'
        title index: true
    }
}
1 dateCreated and lastUpdated are automatically timestamped by GORM.
2 The constraints block defines validation rules and column constraints.
3 The mapping block customises the Hibernate ORM mapping.

12.2. Automatic Properties

Every domain class automatically gets:

Property Description

id

Auto-generated primary key (Long by default)

version

Optimistic locking version column (Long)

And auto-timestamped properties if declared:

Property Description

dateCreated

Set to the current timestamp on first save

lastUpdated

Updated to the current timestamp on every save

Refer to the following sections for details on domain class features:

12.3. Association in GORM

13. GORM Associations

GORM supports all standard relationship types between domain classes. Each association type maps to a specific Hibernate/database pattern.

Association type Declared with

Many-to-one / One-to-one

A property of the target type

One-to-many (bidirectional)

hasMany + belongsTo

One-to-many (unidirectional)

hasMany only (join table)

Many-to-many

hasMany on both sides + belongsTo on one

Refer to the following subsections for details on each association type:

13.1. Many-to-one and one-to-one

14. Many-to-One and One-to-One Associations

14.1. Many-to-One

Declare a many-to-one association by adding a property of the target domain class type:

class Book {
    String title
    Author author           (1)
}

class Author {
    String name
}
1 A foreign key column author_id is added to the book table.

When combined with belongsTo, the association participates in cascade save/delete:

class Book {
    String title
    static belongsTo = [author: Author]     (1)
}
1 The author property is added implicitly; deleting Author cascades to Book.

14.2. One-to-One

A one-to-one association is also declared as a simple property, but each Author can only have one Biography:

class Author {
    String name
    Biography biography      (1)
}

class Biography {
    String summary
    static belongsTo = [author: Author]
}
1 A unique foreign key biography_id is added to author.

14.3. Configuring the Foreign Key Column

Override the foreign key column name in the mapping block:

class Book {
    String title
    Author author
    static mapping = {
        author column: 'fk_author'
    }
}

14.4. Nullable Associations

By default, GORM-managed foreign key columns are non-nullable. To allow null:

class Book {
    Author author

    static constraints = {
        author nullable: true
    }
}

14.4.1. One-to-many

15. One-to-Many Associations

A one-to-many association is declared using hasMany. It represents a collection of associated domain objects.

15.1. Bidirectional One-to-Many

When both sides declare the relationship, GORM manages the foreign key on the many side’s table:

class Author {
    String name
    static hasMany = [books: Book]  (1)
}

class Book {
    String title
    Author author                   (2)
    static belongsTo = [author: Author]
}
1 Author owns a collection of Book objects.
2 Book declares the back-reference. belongsTo also enables cascade delete.

With belongsTo, deleting an Author will cascade-delete all of its books.

15.2. Unidirectional One-to-Many

Without belongsTo on the other side, GORM uses a join table to maintain the relationship:

class Author {
    String name
    static hasMany = [books: Book]
}

class Book {
    String title
    // no belongsTo or author property
}

The join table name defaults to author_books and can be customised — see Table and Column Names.

15.3. Sorting

You can define a default sort order for the collection:

class Author {
    static hasMany = [books: Book]
    static mapping = {
        books sort: 'title', order: 'asc'
    }
}

15.4. Adding and Removing Items

def author = Author.get(1)
author.addToBooks(new Book(title: 'Groovy in Action'))
author.save()

author.removeFromBooks(author.books.first())
author.save()

15.4.1. Many-to-many

16. Many-to-Many Associations

A many-to-many association is created when both domain classes declare hasMany pointing at each other, and one side also declares belongsTo.

16.1. Declaring the Relationship

class Book {
    String title
    static hasMany  = [authors: Author]
    static belongsTo = Author               (1)
}

class Author {
    String name
    static hasMany = [books: Book]
}
1 belongsTo without a property name makes Book the owned side. The owning side (Author) controls cascade save/delete.

GORM creates a join table author_books with foreign-key columns for both sides.

16.2. Saving a Many-to-Many

Always save from the owning side (the side that does not have belongsTo):

def author = new Author(name: 'Graeme Rocher')
def book   = new Book(title:  'Grails in Action')

author.addToBooks(book)
author.save()           (1)
1 book is cascade-saved because Author is the owning side.

16.3. Join Table Customisation

Override the join table name and columns in the mapping block:

class Author {
    static hasMany = [books: Book]
    static mapping = {
        books joinTable: [
            name:   'author_to_book',
            key:    'auth_id',
            column: 'book_id'
        ]
    }
}

16.4. Bidirectional Access

Both sides of the relationship can be navigated:

Author author = Author.get(1)
author.books.each { println it.title }

Book book = Book.get(1)
book.authors.each { println it.name }

16.4.1. Basic Collection Types

17. Basic Collection Types

In addition to collections of domain objects, GORM supports collections of basic types — strings, numbers, enums, and other persistable values. These are stored in a separate join table.

17.1. String Collections

class Author {
    String name
    static hasMany = [nicknames: String]    (1)
}
1 A join table author_nicknames is created with a nicknames column holding the string values.

17.2. Numeric Collections

class Survey {
    String question
    static hasMany = [scores: Integer]
}

17.3. Enum Collections

Collections of enum types are supported:

enum Status { ACTIVE, INACTIVE, SUSPENDED }

class Account {
    String name
    static hasMany = [allowedStatuses: Status]  (1)
}
1 A join table account_allowed_statuses is created. Enum values are stored using their ordinal position by default.

To store enum values as their string names instead of ordinals, configure the enumType on the column:

class Account {
    static hasMany = [allowedStatuses: Status]
    static mapping = {
        allowedStatuses {
            column enumType: 'string'   (1)
        }
    }
}
1 Stores 'ACTIVE', 'INACTIVE', etc. instead of 0, 1, 2.

17.4. Customising the Join Table

You can override the join table name and the value column name:

class Author {
    static hasMany = [nicknames: String]
    static mapping = {
        nicknames joinTable: [
            name:   'author_alias',     (1)
            column: 'alias_value'       (2)
        ]
    }
}
1 The join table name.
2 The column that holds the basic value.

17.5. Accessing and Modifying

Basic collections behave like any other GORM hasMany — use addTo* and removeFrom*:

def author = Author.get(1)
author.addToNicknames('Graeme')
author.save()

author.removeFromNicknames('Graeme')
author.save()

17.6. Composition in GORM

18. GORM Composition (Embedded Objects)

GORM supports composition via embedded, which maps a non-domain class as a set of columns on the owning table rather than a separate table.

18.1. Declaring an Embedded Component

class Address {
    String street
    String city
    String postalCode
    String country
}

class Person {
    String name
    Address address             (1)

    static embedded = ['address']   (2)
}
1 Address is a plain Groovy class (not a domain class).
2 Declaring it in embedded maps its properties as columns on the person table.

The person table will contain columns: name, address_street, address_city, address_postal_code, address_country.

18.2. Overriding Column Names

Use the mapping block to rename the embedded columns:

class Person {
    static embedded = ['address']
    static mapping = {
        address {
            street  column: 'addr_street'
            city    column: 'addr_city'
        }
    }
}

18.3. Nullable Embedded Objects

If the embedded object can be absent, mark it as nullable in constraints:

class Person {
    Address address
    static embedded  = ['address']
    static constraints = {
        address nullable: true
    }
}
Embedded components are always persisted as part of the owning entity. There is no separate table, no id, and no lifecycle management for the embedded object.

18.4. Inheritance in GORM

19. Inheritance in GORM

GORM supports standard Groovy/Java class inheritance. Domain classes in a hierarchy all benefit from GORM persistence.

See Inheritance Strategies for the available mapping strategies (table-per-hierarchy, table-per-subclass, table-per-concrete-class) and how to configure them.

19.1. Basic Inheritance

class Content {
    String title
    Date dateCreated
}

class BlogPost extends Content {
    String body
    String author
}

class Page extends Content {
    String html
    String slug
}

By default all three classes are stored in a single content table (table-per-hierarchy). GORM uses a class discriminator column that stores the fully qualified class name of each row to distinguish rows.

19.2. Querying the Hierarchy

GORM queries are polymorphic by default — querying the parent class returns instances of all subclasses:

List<Content> all = Content.list()      // returns BlogPost and Page instances

List<BlogPost> posts = BlogPost.list()  // returns only BlogPost instances

19.3. Abstract Base Classes

You can use abstract domain classes as the root of a hierarchy. Abstract classes have no corresponding rows and cannot be instantiated directly:

abstract class Content {
    String title
}

class BlogPost extends Content { ... }
Prefer table-per-hierarchy (the default) for most use cases. It requires no JOIN for polymorphic queries and is the most performant strategy.

19.4. Sets, Lists and Maps

20. Sets, Lists and Maps

By default hasMany creates a java.util.Set collection (unordered, no duplicates). GORM also supports List and Map collection types.

20.1. Sets (Default)

class Author {
    static hasMany = [books: Book]  // Set<Book> by default
}

20.2. Lists (Ordered)

Declare the collection property as a List to use a positional, ordered collection. GORM adds an index column to the join table:

class Author {
    List books                      (1)
    static hasMany = [books: Book]
}
1 Declaring the field type as List tells GORM to use a list mapping with a position index.
def author = Author.get(1)
author.books[0]     (1)
1 Access by position — Hibernate maintains the order using an idx column in the join table.

20.3. Maps (Key-Value)

Declare the collection property as a Map to store key-value pairs. The key is typically a String:

class Author {
    Map books                       (1)
    static hasMany = [books: Book]
}
1 Keys and values are stored in the join table.
def author = Author.get(1)
author.books['grailsInAction']      (1)
1 Access by key.

20.4. Sorting Sets

For Set-based collections, define a default sort order in the mapping block:

class Author {
    static hasMany = [books: Book]
    static mapping = {
        books sort: 'title', order: 'asc'
    }
}
List mappings incur the cost of maintaining a positional index column on every insert/reorder. Use them only when ordering matters. For most associations, the default Set is the better choice.

21. Persistence Basics

22. Persistence Basics

This section covers the core persistence operations available on every GORM domain class: saving, updating, deleting, querying for changes, and transaction management.

A key thing to remember about GORM is that under the surface GORM is using Hibernate for persistence. If you are coming from a background of using ActiveRecord or iBatis/MyBatis, Hibernate’s "session" model may feel a little strange.

If you are using Grails, then Grails automatically binds a Hibernate session to the currently executing request. This lets you use the save() and delete() methods as well as other GORM methods transparently.

If you are not using Grails then you have to make sure that a session is bound to the current request. One way to achieve that is with the withNewSession(Closure) method:

Book.withNewSession {
    // your logic here
}

Another option is to bind a transaction using the withTransaction(Closure) method:

Book.withTransaction {
    // your logic here
}
Hibernate 7 — direct Session API changes. If you call the Hibernate Session directly inside a withSession block (rather than using GORM’s own methods), note that session.save(), session.update(), session.delete(), session.load(), and session.get() were removed in Hibernate 7. Use the JPA equivalents: session.persist(), session.merge(), session.remove(), session.getReference(), and session.find() respectively. GORM’s dynamic methods (save(), delete(), get(), etc. on the domain class) are not affected — they go through GORM’s own API and work unchanged.

22.1. Transactional Write-Behind

A useful feature of Hibernate over direct JDBC calls and even other frameworks is that when you call save() or delete() it does not necessarily perform any SQL operations at that point. Hibernate batches up SQL statements and executes them as late as possible, often at the end of the request when flushing and closing the session.

If you are using Grails this is typically done for you automatically, which manages your Hibernate session. If you are using GORM outside of Grails then you may need to manually flush the session at the end of your operation.

Hibernate caches database updates where possible, only actually pushing the changes when it knows that a flush is required, or when a flush is triggered programmatically. One common case where Hibernate will flush cached updates is when performing queries since the cached information might be included in the query results. But as long as you’re doing non-conflicting saves, updates, and deletes, they’ll be batched until the session is flushed. This can be a significant performance boost for applications that do a lot of database writes.

Note that flushing is not the same as committing a transaction. If your actions are performed in the context of a transaction, flushing will execute SQL updates but the database will save the changes in its transaction queue and only finalize the updates when the transaction commits.

22.2. Saving and Updating

23. Saving and Updating

23.1. Saving

Call save() on a domain instance to persist it. GORM delegates to Hibernate’s Session.saveOrUpdate():

def book = new Book(title: 'Groovy in Action', author: 'Dierk König')
book.save()

If validation fails, save() returns null and the errors are available on the instance:

def book = new Book(title: '')   // violates blank constraint
if (!book.save()) {
    book.errors.allErrors.each { println it }
}

23.2. Fail on Error

Use failOnError: true to throw a ValidationException instead of returning null:

book.save(failOnError: true)    // throws ValidationException on constraint violation

23.3. Flush

By default Hibernate delays SQL writes until the session is flushed. Force an immediate flush:

book.save(flush: true)      (1)
1 Issues the INSERT or UPDATE immediately.

23.4. Updating

Modify properties on a loaded instance and call save():

def book = Book.get(1)
book.title = 'Updated Title'
book.save()

23.5. Dynamic Update

To generate UPDATE statements that only include changed columns (useful for wide tables), enable dynamicUpdate:

class Book {
    static mapping = {
        dynamicUpdate true
    }
}

23.6. Dynamic Insert

Similarly, dynamicInsert generates INSERT statements that omit null properties:

static mapping = {
    dynamicInsert true
}

23.7. Deleting Objects

24. Deleting Objects

Call delete() on a loaded instance to remove it from the database:

def book = Book.get(1)
book.delete()

24.1. Flush on Delete

book.delete(flush: true)    // issues DELETE immediately

24.2. Cascade Delete

When a domain class declares belongsTo, deleting the parent also deletes its children:

class Author {
    static hasMany = [books: Book]
}

class Book {
    static belongsTo = [author: Author]
}

// Deletes the author AND all associated books
Author.get(1).delete()

To delete without cascading, remove the belongsTo and configure cascade behaviour explicitly — see Custom Cascade Behaviour.

24.3. Bulk Delete

Use deleteAll() to delete all instances matching a criteria:

Book.where { genre == 'Horror' }.deleteAll()

Or with HQL:

Book.executeUpdate("delete Book where genre = :genre", [genre: 'Horror'])

24.4. Understanding Cascading Updates and Deletes

25. Cascades

Hibernate cascades propagate persistence operations from a parent entity to its associated children automatically.

See Custom Cascade Behaviour for the full reference on configuring cascade behaviour via the mapping block.

25.1. Default Cascade Behaviour

GORM applies save-update cascading by default on associations managed by hasMany. This means:

  • Saving an Author also saves any new or modified Book objects in its books collection.

  • Deleting an Author does NOT automatically delete its books unless belongsTo is declared or cascade: 'all' is configured.

25.2. Cascade with belongsTo

Declaring belongsTo on the owned side automatically adds cascade-delete from the owning side:

class Book {
    static belongsTo = [author: Author]     (1)
}
1 Deleting an Author cascade-deletes all associated Book rows.

25.3. Cascade with all-delete-orphan

Use all-delete-orphan to delete child rows that are removed from the collection:

class Author {
    static hasMany = [books: Book]
    static mapping = {
        books cascade: 'all-delete-orphan'
    }
}

def author = Author.get(1)
author.books.remove(author.books.first())   (1)
author.save()
1 The removed Book will be deleted from the database.

25.4. Eager and Lazy Fetching

26. Fetching

Controlling how and when associated data is loaded is critical for application performance.

See Fetching Strategies for full configuration details.

26.1. Default: Lazy Loading

Associations are loaded lazily by default — Hibernate does not query associated data until you access it:

def author = Author.get(1)      // SELECT * FROM author WHERE id=1
author.books.size()             // SELECT * FROM book WHERE author_id=1 (triggered now)

26.2. N+1 Problem

Loading a list of authors and accessing their books triggers one query per author:

Author.list().each { author ->
    println author.books.size()     (1)
}
1 N additional queries for N authors — the N+1 problem.

26.3. Solution: Eager Fetching with Join

// Option 1: query-time join
def authors = Author.findAll {
    join 'books'
}

// Option 2: mapping-level eager fetch (always eager — use with care)
static mapping = {
    books fetch: 'join'
}

26.4. Batch Fetching

A lighter alternative to join — fetch collections in batches to reduce query count without a cartesian product:

static mapping = {
    books batchSize: 10     (1)
}
1 Hibernate will initialise up to 10 book collections with a single IN query.

26.5. Pessimistic and Optimistic Locking

27. Locking

27.1. Optimistic Locking

GORM enables optimistic locking by default via a version column. See Optimistic Locking and Versioning for full details.

27.2. Pessimistic Locking

Pessimistic locking acquires a database row lock, typically using SQL SELECT ... FOR UPDATE. Conflicting writes and lock requests may block; ordinary reads are not necessarily blocked. The exact behavior depends on the database and transaction isolation level.

An active transaction is required. Use withTransaction as below or a GORM @Transactional method, keeping the lock and the work it protects in the same transaction. The lock is held until that transaction commits or rolls back. Locking without an active transaction throws jakarta.persistence.TransactionRequiredException before anything is loaded; this applies to refresh(lock: ...) and to every form of lock(id, ...).

To load an entity by identifier under a lock, use the static lock(id) method:

Book.withTransaction {
    def book = Book.lock(1)
    book.title = 'Updated Safely'
    book.save(failOnError: true)
}

The static Book.lock(id) method is unchanged, and Book.lock(id, refresh: false) behaves the same way. It avoids a separate unlocked get() when the entity is not already managed, but when the entity is already managed in the current session it locks and version-checks the already-loaded state rather than reloading it. Pass refresh: true to reload that instance’s state and version under the lock instead:

Book.withTransaction {
    def book = Book.lock(1, refresh: true)
    if (book?.title == 'Draft') {
        book.title = 'Ready for review'
        book.save(failOnError: true)
    }
}

Book.lock(id, refresh: true) returns the same managed instance when the entity is already in the session, loads and locks the entity when it is not, and returns null when no row exists for the identifier. It has the same transaction requirement as refresh(lock: true), discards unflushed changes in the same way, and throws UnsupportedOperationException on datastores that do not support it.

Both static forms accept type to choose the lock mode: a jakarta.persistence.LockModeType or its name, defaulting to PESSIMISTIC_WRITE (NONE is rejected). Book.lock(1, type: LockModeType.PESSIMISTIC_READ) acquires a pessimistic READ lock whether the entity is loaded by the call or already managed, and Book.lock(1, refresh: true, type: LockModeType.PESSIMISTIC_READ) reloads a managed instance under that mode. Datastores that support only a pessimistic write lock throw UnsupportedOperationException for any other type.

Every lock mode is available to refresh(lock: ...) and to the static type argument:

Lock mode Database lock Effect

PESSIMISTIC_WRITE

Exclusive row lock, typically SELECT ... FOR UPDATE

The default for lock: true, lock(id) and lock(id, refresh: true). Other transactions cannot update the row or take a pessimistic lock on it until this transaction ends.

PESSIMISTIC_READ

Shared row lock, SELECT ... FOR SHARE where the database supports it

Other transactions cannot update the row but may take the same shared lock. Databases without shared row locks take an exclusive lock instead.

PESSIMISTIC_FORCE_INCREMENT

Exclusive row lock

As PESSIMISTIC_WRITE, and the version is incremented as part of taking the lock, so a transaction still holding the previous version fails its next update.

OPTIMISTIC (JPA alias READ)

None

The version is re-read when the transaction commits, and the commit fails if another transaction changed it.

OPTIMISTIC_FORCE_INCREMENT (JPA alias WRITE)

None

The version is incremented when the transaction commits, so other transactions holding the previous version fail.

NONE

None

refresh(lock: LockModeType.NONE) is a plain refresh, exactly like refresh(lock: false). lock(id, type: LockModeType.NONE) is rejected with IllegalArgumentException: a lock call must name a lock.

In a hierarchy mapped with tablePerConcreteClass true, Hibernate 7 locks through a union of the concrete tables whenever it goes through the hierarchy root, and some databases, H2 among them, do not lock rows through such a query. refresh(lock: true) and lock(id, refresh: true) on a managed instance of a class without subclasses lock its concrete table directly, as does lock(id) through that class for a row not yet loaded.

You can also lock an already-loaded instance:

Book.withTransaction {
    def book = Book.get(1)
    book.lock() // lock without refreshing
    book.title = 'Updated title'
    book.save(failOnError: true)
}

The instance method book.lock() is unchanged: it acquires a lock without reloading the entity’s state. For an already-loaded, versioned entity, Hibernate still checks its version against the database. Another transaction can update the row between get() and lock(), causing an optimistic locking failure.

27.2.1. Refreshing While Locking

To reload an already-loaded entity’s database state and version directly under a pessimistic WRITE lock, pass lock: true to the refresh(Map) instance method. The lock is acquired before the state is read, so the reload sees the committed state the lock protects. Unlike calling lock() first, the already-loaded version is never checked, and unlike calling refresh() first, no competing update can slip in between. The same instance is returned.

Book.withTransaction {
    def book = Book.get(1)
    book.refresh(lock: true)
    if (book.title == 'Draft') {
        book.title = 'Ready for review'
        book.save(failOnError: true)
    }
}

To request a lock other than a pessimistic WRITE lock, pass a jakarta.persistence.LockModeType (or its name) as the lock argument, for example book.refresh(lock: LockModeType.PESSIMISTIC_READ). lock: true is equivalent to lock: LockModeType.PESSIMISTIC_WRITE, and the semantics of each mode are those of the JPA LockModeType of the same name. A value that is neither a boolean, a LockModeType, nor the name of one throws IllegalArgumentException.

An optimistic mode requested while the transaction already holds a stronger lock on the same instance is superseded by that lock, so no version check or increment is registered for it.

The instance must be attached to the current session; refreshing a detached instance under a lock throws IllegalArgumentException, so re-attach it with attach() first. book.refresh() is unchanged, and book.refresh([:]), book.refresh(lock: false) or book.refresh(lock: LockModeType.NONE) behave exactly like it: a plain refresh with no lock and no transaction requirement. To lock by identifier and reload at the same time, use Book.lock(id, refresh: true) as described above.

There is no book.lock(refresh: true). Groovy resolves that call to the static lock(Serializable id) method with the options map as the identifier, and GORM rejects it with an IllegalArgumentException that names the supported forms. The instance form is book.refresh(lock: true).
Refreshing discards unflushed changes to the entity. Configured refresh cascades can also discard unflushed changes to associated entities; this is not a refresh of the whole object graph. Call refresh(lock: true) before making decisions based on the entity’s state or applying mutations. It does not preserve or merge pending edits.

The lock applies to the refreshed entity’s own row. Associated entities reloaded through a refresh cascade are not guaranteed to be locked: databases that cannot lock outer-joined rows, such as H2 and PostgreSQL, leave those rows unlocked, and GORM does not issue follow-on locking queries for them.

Refreshing under a lock is supported by GORM for Hibernate 5 and Hibernate 7. Datastores that do not support it throw UnsupportedOperationException rather than silently falling back to a plain refresh.

The database and transaction isolation level determine which state is visible to the locked refresh. refresh(lock: true) does not override isolation rules or guarantee that every concurrent attempt succeeds. Deadlocks, lock timeouts, and transaction serialization failures remain possible.

27.2.2. Holding the Lock for a Block of Work

mutex(Closure) locks the instance exclusively for the scope of the closure and returns the closure’s result:

Airport.withTransaction {
    def airport = Airport.get(10)
    airport.mutex {
        airport.name = 'Heathrow'
        airport.save(failOnError: true)
    }
}

It reloads the instance’s state and version under the lock, so a transaction that committed to the row after it was loaded is waited for rather than reported as an optimistic locking failure, and the closure runs on the committed state. The lock is always exclusive; there is no argument to select another mode, because a shared or optimistic lock would not give the closure mutual exclusion.

Reloading discards unflushed changes to the instance, exactly as refresh(lock: true) does. Make changes inside the closure, not before the call. An active transaction and an attached instance are required, as they are for refresh(lock: true).

27.3. Refresh

To reload state from the database without requesting a pessimistic lock (discarding unflushed in-memory changes), use refresh():

Book.withTransaction {
    def book = Book.get(1)
    book.refresh()
}

Read visibility still depends on transaction isolation. book.refresh([:]) and book.refresh(lock: false) behave exactly like book.refresh(). If subsequent decisions or mutations require a reload protected by a write lock, use book.refresh(lock: true) instead of separate refresh() and lock() calls.

27.4. Modification Checking

28. Modification Checking

Hibernate tracks which properties have been modified since the entity was loaded. GORM exposes this via isDirty() and related methods.

28.1. Checking if an Instance is Dirty

def book = Book.get(1)
book.isDirty()      // false — just loaded

book.title = 'New Title'
book.isDirty()      // true — title has changed

28.2. Checking a Specific Property

book.isDirty('title')   // true
book.isDirty('genre')   // false — genre unchanged

28.3. Getting the Original Value

def book = Book.get(1)
println book.title          // 'Original Title'
book.title = 'New Title'
println book.getPersistentValue('title')    // 'Original Title'

28.4. Dirty Properties

Get a list of all property names that have changed:

def book = Book.get(1)
book.title = 'New Title'
book.genre = 'Fiction'
println book.dirtyPropertyNames    // ['title', 'genre']

29. Querying with GORM

30. Querying

GORM provides multiple querying mechanisms, ranging from simple dynamic finders to full SQL queries.

30.1. Dynamic Finders

The simplest form of querying uses auto-generated finder methods based on property names:

Book.findByTitle('Groovy in Action')
Book.findAllByAuthorAndGenre('Dierk König', 'Tech')
Book.countByGenre('Fiction')
Book.findByTitleLike('%Groovy%')
Book.findAllByPagesGreaterThan(300)
Book.findAllByTitleIlike('%groovy%')    // case-insensitive

Finder methods support pagination:

Book.findAllByGenre('Fiction', [max: 10, offset: 0, sort: 'title', order: 'asc'])

30.2. get, list, count

Book.get(1)                                 // by id
Book.list()                                 // all
Book.list(max: 10, offset: 20)             // paginated
Book.count()                                // total count

30.3. Dynamic Finders

GORM supports the concept of dynamic finders. A dynamic finder looks like a static method invocation, but the methods themselves don’t actually exist in any form at the code level.

Instead, a method is auto-magically generated using code synthesis at runtime, based on the properties of a given class. Take for example the Book class:

class Book {
    String title
    Date releaseDate
    Author author
}
class Author {
    String name
}

The Book class has properties such as title, releaseDate and author. These can be used by the findBy* and findAllBy* methods in the form of "method expressions":

def book = Book.findByTitle("The Stand")

book = Book.findByTitleLike("Harry Pot%")

book = Book.findByReleaseDateBetween(firstDate, secondDate)

book = Book.findByReleaseDateGreaterThan(someDate)

book = Book.findByTitleLikeOrReleaseDateLessThan("%Something%", someDate)

30.3.1. Method Expressions

A method expression in GORM is made up of the prefix such as findBy* followed by an expression that combines one or more properties. The basic form is:

Book.findBy(<<Property>><<Comparator>><<Boolean Operator>>)?<<Property>><<Comparator>>

The tokens marked with a ? are optional. Each comparator changes the nature of the query. For example:

def book = Book.findByTitle("The Stand")

book =  Book.findByTitleLike("Harry Pot%")

In the above example the first query is equivalent to equality whilst the latter, due to the Like comparator, is equivalent to a SQL like expression.

The possible comparators include:

  • InList - In the list of given values

  • LessThan - less than a given value

  • LessThanEquals - less than or equal a give value

  • GreaterThan - greater than a given value

  • GreaterThanEquals - greater than or equal a given value

  • Like - Equivalent to a SQL like expression

  • Ilike - Similar to a Like, except case insensitive

  • NotEqual - Negates equality

  • InRange - Between the from and to values of a Groovy Range

  • Rlike - Performs a Regexp LIKE in MySQL or Oracle otherwise falls back to Like

  • Between - Between two values (requires two arguments)

  • IsNotNull - Not a null value (doesn’t take an argument)

  • IsNull - Is a null value (doesn’t take an argument)

Notice that the last three require different numbers of method arguments compared to the rest, as demonstrated in the following example:

def now = new Date()
def lastWeek = now - 7
def book = Book.findByReleaseDateBetween(lastWeek, now)

books = Book.findAllByReleaseDateIsNull()
books = Book.findAllByReleaseDateIsNotNull()

30.3.2. Boolean logic (AND/OR)

Method expressions can also use a boolean operator to combine two or more criteria:

def books = Book.findAllByTitleLikeAndReleaseDateGreaterThan(
                      "%Java%", new Date() - 30)

In this case we’re using And in the middle of the query to make sure both conditions are satisfied, but you could equally use Or:

def books = Book.findAllByTitleLikeOrReleaseDateGreaterThan(
                      "%Java%", new Date() - 30)

You can combine as many criteria as you like, but they must all be combined with And or all Or. If you need to combine And and Or or if the number of criteria creates a very long method name, just convert the query to a Criteria or HQL query.

30.3.3. Querying Associations

Associations can also be used within queries:

def author = Author.findByName("Stephen King")

def books = author ? Book.findAllByAuthor(author) : []

In this case if the Author instance is not null we use it in a query to obtain all the Book instances for the given Author.

30.3.4. Pagination and Sorting

The same pagination and sorting parameters available on the list() method can also be used with dynamic finders by supplying a map as the final parameter:

def books = Book.findAllByTitleLike("Harry Pot%",
               [max: 3, offset: 2, sort: "title", order: "desc"])

The sort value must be a property path made up of identifiers separated by dots, such as "title" or "author.name", that resolves through the domain class mapping. Anything else, for example a value carrying a second expression or a function call, is rejected with an IllegalArgumentException before the query is built, so request parameters can be passed through safely. A dotted key whose first segment is not a property of the domain class is left for the query implementation to resolve, so aliases created with createAlias or declared in a where query can still be sorted on; a bare name must be a property of the domain class. The order value must be "asc" or "desc", case-insensitive and ignoring surrounding whitespace; anything else is rejected in the same way. The same checks apply to the sort and order arguments of list(), of a criteria query’s list() call and of a where query’s list() call, and to the order argument of listOrderBy*, so a value is never silently coerced on one entry point and rejected on another.

30.4. Where Queries

The where() method builds on the support for Detached Criteria by providing an enhanced, compile-time checked query DSL for common queries. The where method is more flexible than dynamic finders, less verbose than criteria and provides a powerful mechanism to compose queries.

30.4.1. Basic Querying

The where() method accepts a closure that looks very similar to Groovy’s regular collection methods. The closure should define the logical criteria in regular Groovy syntax, for example:

def query = Person.where {
   firstName == "Bart"
}
Person bart = query.find()

The returned object is a DetachedCriteria instance, which means it is not associated with any particular database connection or session. This means you can use the where method to define common queries at the class level:

import grails.gorm.*

class Person {
    static DetachedCriteria<Person> simpsons = where {
         lastName == "Simpson"
    }
    ...
}
...
Person.simpsons.each { Person p ->
    println p.firstname
}

Query execution is lazy and only happens upon usage of the DetachedCriteria instance. If you want to execute a where-style query immediately there are variations of the findAll and find methods to accomplish this:

def results = Person.findAll {
     lastName == "Simpson"
}
def results = Person.findAll(sort:"firstName") {
     lastName == "Simpson"
}
Person p = Person.find { firstName == "Bart" }

Each Groovy operator maps onto a regular criteria method. The following table provides a map of Groovy operators to methods:

Operator Criteria Method Description

==

eq

Equal to

!=

ne

Not equal to

>

gt

Greater than

<

lt

Less than

>=

ge

Greater than or equal to

<=

le

Less than or equal to

in

inList

Contained within the given list

==~

like

Like a given string

=~

ilike

Case insensitive like

It is possible use regular Groovy comparison operators and logic to formulate complex queries:

def query = Person.where {
    (lastName != "Simpson" && firstName != "Fred") || (firstName == "Bart" && age > 9)
}
def results = query.list(sort:"firstName")

The Groovy regex matching operators map onto like and ilike queries unless the expression on the right hand side is a Pattern object, in which case they map onto an rlike query:

def query = Person.where {
     firstName ==~ ~/B.+/
}
Note that rlike queries are only supported if the underlying database supports regular expressions

A between criteria query can be done by combining the in keyword with a range:

def query = Person.where {
     age in 18..65
}

Finally, you can do isNull and isNotNull style queries by using null with regular comparison operators:

def query = Person.where {
     middleName == null
}

30.4.2. Query Composition

Since the return value of the where method is a DetachedCriteria instance you can compose new queries from the original query:

DetachedCriteria<Person> query = Person.where {
     lastName == "Simpson"
}
DetachedCriteria<Person> bartQuery = query.where {
     firstName == "Bart"
}
Person p = bartQuery.find()

Note that you cannot pass a closure defined as a variable into the where method unless it has been explicitly cast to a DetachedCriteria instance. In other words the following will produce an error:

def callable = {
    lastName == "Simpson"
}
def query = Person.where(callable)

The above must be written as follows:

import grails.gorm.DetachedCriteria

def callable = {
    lastName == "Simpson"
} as DetachedCriteria<Person>
def query = Person.where(callable)

As you can see the closure definition is cast (using the Groovy as keyword) to a DetachedCriteria instance targeted at the Person class.

30.4.3. Conjunction, Disjunction and Negation

As mentioned previously you can combine regular Groovy logical operators (|| and &&) to form conjunctions and disjunctions:

def query = Person.where {
    (lastName != "Simpson" && firstName != "Fred") || (firstName == "Bart" && age > 9)
}

You can also negate a logical comparison using !:

def query = Person.where {
    firstName == "Fred" && !(lastName == 'Simpson')
}

30.4.4. Property Comparison Queries

If you use a property name on both the left hand and right side of a comparison expression then the appropriate property comparison criteria is automatically used:

def query = Person.where {
   firstName == lastName
}

The following table described how each comparison operator maps onto each criteria property comparison method:

Operator Criteria Method Description

==

eqProperty

Equal to

!=

neProperty

Not equal to

>

gtProperty

Greater than

<

ltProperty

Less than

>=

geProperty

Greater than or equal to

⇐

leProperty

Less than or equal to

30.4.5. Querying Associations

Associations can be queried by using the dot operator to specify the property name of the association to be queried:

def query = Pet.where {
    owner.firstName == "Joe" || owner.firstName == "Fred"
}

You can group multiple criterion inside a closure method call where the name of the method matches the association name:

def query = Person.where {
    pets { name == "Jack" || name == "Joe" }
}

This technique can be combined with other top-level criteria:

def query = Person.where {
     pets { name == "Jack" } || firstName == "Ed"
}

For collection associations it is possible to apply queries to the size of the collection:

def query = Person.where {
       pets.size() == 2
}

The following table shows which operator maps onto which criteria method for each size() comparison:

Operator Criteria Method Description

==

sizeEq

The collection size is equal to

!=

sizeNe

The collection size is not equal to

>

sizeGt

The collection size is greater than

<

sizeLt

The collection size is less than

>=

sizeGe

The collection size is greater than or equal to

⇐

sizeLe

The collection size is less than or equal to

30.4.6. Query Aliases and Sorting

If you define a query for an association an alias is automatically generated for the query. For example the following query:

def query = Pet.where {
    owner.firstName == "Fred"
}

Will generate an alias for the owner association such as owner_alias_0. These generated aliases are fine for most cases, but are not useful if you want to later sort or use a projection on the results. For example the following query will fail:

// fails because a dynamic alias is used
Pet.where {
    owner.firstName == "Fred"
}.list(sort:"owner.lastName")

If you plan to sort the results then an explicit alias should be used and these can be defined by simply declaring a variable in the where query:

def query = Pet.where {
    def o1 = owner (1)
    o1.firstName == "Fred" (2)
}.list(sort:'o1.lastName') (3)
1 Define an alias called o1
2 Use the alias in the query itself
3 Use the alias to sort the results

By assigning the name of an association to a local variable it will automatically become an alias usable within the query itself and also for the purposes of sorting or projecting the results.

30.4.7. Subqueries

It is possible to execute subqueries within where queries. For example to find all the people older than the average age the following query can be used:

final query = Person.where {
  age > avg(age)
}

The following table lists the possible subqueries:

Method Description

avg

The average of all values

sum

The sum of all values

max

The maximum value

min

The minimum value

count

The count of all values

property

Retrieves a property of the resulting entities

You can apply additional criteria to any subquery by using the of method and passing in a closure containing the criteria:

def query = Person.where {
  age > avg(age).of { lastName == "Simpson" } && firstName == "Homer"
}

Since the property subquery returns multiple results, the criterion used compares all results. For example the following query will find all people younger than people with the surname "Simpson":

Person.where {
    age < property(age).of { lastName == "Simpson" }
}

30.4.8. More Advanced Subqueries in GORM

The support for subqueries has been extended. You can now use in with nested subqueries

def results = Person.where {
    firstName in where { age < 18 }.firstName
}.list()

Criteria and where queries can be seamlessly mixed:

def results = Person.withCriteria {
    notIn "firstName", Person.where { age < 18 }.firstName
 }

Subqueries can be used with projections:

def results = Person.where {
    age > where { age > 18 }.avg('age')
}

Correlated queries that span two domain classes can be used:

def employees = Employee.where {
    region.continent in ['APAC', "EMEA"]
    }.id()
    def results = Sale.where {
    employee in employees && total > 100000
    }.employee.list()

And support for aliases (cross query references) using simple variable declarations has been added to where queries:

def query = Employee.where {
    def em1 = Employee
    exists Sale.where {
        def s1 = Sale
        def em2 = employee
        return em2.id == em1.id
    }.id()
}
def results = query.list()

30.4.9. Other Functions

There are several functions available to you within the context of a query. These are summarized in the table below:

Method Description

second

The second of a date property

minute

The minute of a date property

hour

The hour of a date property

day

The day of the month of a date property

month

The month of a date property

year

The year of a date property

lower

Converts a string property to lower case

upper

Converts a string property to upper case

length

The length of a string property

trim

Trims a string property

Currently functions can only be applied to properties or associations of domain classes. You cannot, for example, use a function on a result of a subquery.

For example the following query can be used to find all pet’s born in 2011:

def query = Pet.where {
    year(birthDate) == 2011
}

You can also apply functions to associations:

def query = Person.where {
    year(pets.birthDate) == 2009
}

30.4.10. Batch Updates and Deletes

Since each where method call returns a DetachedCriteria instance, you can use where queries to execute batch operations such as batch updates and deletes. For example, the following query will update all people with the surname "Simpson" to have the surname "Bloggs":

DetachedCriteria<Person> query = Person.where {
    lastName == 'Simpson'
}
int total = query.updateAll(lastName:"Bloggs")
Note that one limitation with regards to batch operations is that join queries (queries that query associations) are not allowed.

To batch delete records you can use the deleteAll method:

DetachedCriteria<Person> query = Person.where {
    lastName == 'Simpson'
}
int total = query.deleteAll()

30.5. Criteria Queries

Criteria is an advanced way to query that uses a Groovy builder to construct potentially complex queries. It is a much better approach than building up query strings using a StringBuilder.

Criteria can be used either with the createCriteria() or withCriteria(closure) methods.

The builder uses Hibernate 7’s Criteria API (based on JPA Criteria). The nodes on this builder map to GORM query constraints which are then translated into JPA Criteria predicates.

def c = Account.createCriteria()
def results = c.list {
    between("balance", 500, 1000)
    eq("branch", "London")
    or {
        like("holderFirstName", "Fred%")
        like("holderFirstName", "Barney%")
    }
    maxResults(10)
    order("holderLastName", "desc")
}

This criteria will select up to 10 Account objects in a List matching the following criteria:

  • balance is between 500 and 1000

  • branch is London

  • holderFirstName starts with Fred or Barney

The results will be sorted in descending order by holderLastName.

If no records are found with the above criteria, an empty List is returned.

30.5.1. Hibernate Criteria DSL

In Hibernate 7, the criteria builder has been completely rewritten to leverage the JPA Criteria API. The HibernateCriteriaBuilder implements the GORM criteria DSL and provides seamless integration with Hibernate 7 features.

The DSL supports all standard GORM criteria methods such as eq, ne, gt, lt, ge, le, between, like, ilike, in, isNull, isNotNull, isEmpty, isNotEmpty, and more.

def results = Person.withCriteria {
    ilike('firstName', 'b%')
    or {
        ge('age', 18)
        isNull('parent')
    }
}

30.5.2. Conjunctions and Disjunctions

As demonstrated in the previous example you can group criteria in a logical OR using an or { } block:

or {
    between("balance", 500, 1000)
    eq("branch", "London")
}

This also works with logical AND:

and {
    between("balance", 500, 1000)
    eq("branch", "London")
}

And you can also negate using logical NOT:

not {
    between("balance", 500, 1000)
    eq("branch", "London")
}

All top level conditions are implied to be AND’d together.

30.5.3. Querying Associations

Associations can be queried by having a node that matches the property name. For example say the Account class had many Transaction objects:

class Account {
    ...
    static hasMany = [transactions: Transaction]
    ...
}

We can query this association by using the property name transactions as a builder node:

def c = Account.createCriteria()
def now = new Date()
def results = c.list {
    transactions {
        between('date', now - 10, now)
    }
}

The above code will find all the Account instances that have performed transactions within the last 10 days. You can also nest such association queries within logical blocks:

def c = Account.createCriteria()
def now = new Date()
def results = c.list {
    or {
        between('created', now - 10, now)
        transactions {
            between('date', now - 10, now)
        }
    }
}

Here we find all accounts that have either performed transactions in the last 10 days OR have been recently created in the last 10 days.

30.5.4. Querying with Projections

Projections may be used to customise the results. Define a "projections" node within the criteria builder tree to use projections.

def c = Account.createCriteria()

def numberOfBranches = c.get {
    projections {
        countDistinct('branch')
    }
}

When multiple fields are specified in the projection, a List of values will be returned. A single value will be returned otherwise.

Common projections include count, countDistinct, groupProperty, avg, min, max, sum, id, and property.

30.5.5. SQL Restrictions

You can restrict the results with a native SQL condition, which is added to the where clause of the query as written, in parentheses.

def c = Person.createCriteria()

def peopleWithShortFirstNames = c.list {
    sqlRestriction "char_length(first_name) <= 4"
}

SQL Restrictions may be parameterized to deal with SQL injection vulnerabilities related to dynamic restrictions.

def c = Person.createCriteria()

def peopleWithShortFirstNames = c.list {
    sqlRestriction "char_length(first_name) < ? AND char_length(first_name) > ?", [maxValue, minValue]
}
Note that the parameter there is SQL. The first_name attribute referenced in the example refers to the persistence model, not the object model like in HQL queries.

Each ? in the SQL is bound to the value at the same position in the list, so the number of ? placeholders must match the number of values, and a value must not be null. A ? in a string literal, such as '%?', in a quoted identifier or in a comment is not a placeholder.

When the query joins other tables, qualify the columns with {alias}, which stands for the table alias of the queried class, so that a column a joined table also has is not ambiguous:

def c = Person.createCriteria()

def peopleWithShortFirstNames = c.list {
    pets {
        eq 'name', 'Lucky'
    }
    sqlRestriction "char_length({alias}.first_name) <= ?", [4]
}

Inside an association block, such as pets { ... }, the condition restricts the association, as the other criteria in the block do, and {alias} stands for the table alias of the association instead. This includes an association block inside an and, or or not block.

A detached criteria accepts sqlRestriction too.

30.5.6. Setting Properties in the Criteria Instance

The builder allows setting properties that control how the query is executed.

import org.hibernate.FetchMode
...
def results = c.list {
    maxResults(10)
    firstResult(50)
    cache(true)
    readOnly(true)
    lock(true)
    fetchMode("aRelationship", FetchMode.JOIN)
}

If a node within the builder tree doesn’t match a particular criterion it will attempt to set a property on the Criteria object itself. This allows full access to all the properties in the criteria.

30.5.7. Advanced Hibernate 7 Features

The Hibernate 7 criteria builder supports several advanced features:

  • Pessimistic Locking: Use lock(true) to obtain a pessimistic write lock.

  • Query Caching: Use cache(true) to enable query caching for the results.

  • Read-Only Mode: Use readOnly(true) to disable dirty checking for loaded entities.

  • Fetch Mode: Use fetchMode("association", FetchMode.JOIN) to specify Eager/Lazy fetching strategies.

30.5.8. Using Scrollable Results

You can use Hibernate’s ScrollableResults feature by calling the scroll method:

def results = crit.scroll {
    maxResults(10)
}
def f = results.first()
def l = results.last()
def n = results.next()
def p = results.previous()

def future = results.scroll(10)
def accountNumber = results.getLong('number')

To quote the Hibernate documentation on ScrollableResults:

A result iterator that allows moving around within the results by arbitrary increments. The Query / ScrollableResults pattern is very similar to the JDBC PreparedStatement / ResultSet pattern and the semantics of methods of this interface are similar to the similarly named methods on ResultSet.

Contrary to JDBC, columns of results are numbered from zero.

30.5.9. Querying with Eager Fetching

In the section on eager and lazy fetching we discussed how to declaratively specify fetching to avoid the N+1 SELECT problem. However, this can also be achieved using a criteria query:

def criteria = Task.createCriteria()
def tasks = criteria.list{
    eq "assignee.id", task.assignee.id
    join 'assignee'
    join 'project'
    order 'priority', 'asc'
}

Notice the usage of the join method: it tells the criteria API to use a JOIN to fetch the named associations with the Task instances. It’s probably best not to use this for one-to-many associations though, because you will most likely end up with duplicate results. Instead, use the select fetch mode:

import org.hibernate.FetchMode as FM
...
def results = Airport.withCriteria {
    eq "region", "EMEA"
    fetchMode "flights", FM.SELECT
}

Although this approach triggers a second query to get the flights association, you will get reliable results — even with the maxResults option.

fetchMode and join are general settings of the query and can only be specified at the top-level, i.e. you cannot use them inside projections or association constraints.

An important point to bear in mind is that if you include associations in the query constraints, those associations will automatically be eagerly loaded. For example, in this query:

def results = Airport.withCriteria {
    eq "region", "EMEA"
    flights {
        like "number", "BA%"
    }
}

the flights collection would be loaded eagerly via a join even though the fetch mode has not been explicitly set.

30.5.10. Method Reference

If you invoke the builder with no method name such as:

c { ... }

The builder defaults to listing all the results and hence the above is equivalent to:

c.list { ... }
Method Description

list

This is the default method. It returns all matching rows.

get

Returns a unique result set i.e. just one row. The criteria has to be formed in a way that it only queries one row. This method is not to be confused with a limit to just the first row.

scroll

Returns a scrollable result set.

listDistinct

If subqueries or associations are used one may end up with the same row multiple times in the result set. This allows listing only distinct entities and is equivalent to DISTINCT_ROOT_ENTITY of the CriteriaSpecification class.

count

Returns the number of matching rows.

30.5.11. Combining Criteria

You can combine multiple criteria closures in the following way:

def emeaCriteria = {
    eq "region", "EMEA"
}

def results = Airport.withCriteria {
    emeaCriteria.delegate = delegate
    emeaCriteria()
    flights {
        like "number", "BA%"
    }
}

This technique requires that each criteria must refer to the same domain class (i.e. Airport). A more flexible approach is to use Detached Criteria, as described in the following section.

30.6. Detached Criteria

30.7. Detached Criteria

Detached Criteria are criteria queries that are not associated with any given database session/connection. Detached Criteria queries have many uses including allowing you to create common reusable criteria queries, execute subqueries and execute batch updates/deletes.

30.7.1. Building Detached Criteria Queries

The primary point of entry for using the Detached Criteria is the grails.gorm.DetachedCriteria class which accepts a domain class as the only argument to its constructor:

import grails.gorm.*
...
def criteria = new DetachedCriteria(Person)

Once you have obtained a reference to a detached criteria instance you can execute where queries or criteria queries to build up the appropriate query. To build a normal criteria query you can use the build method:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}

Note that methods on the DetachedCriteria instance do not mutate the original object but instead return a new query. In other words, you have to use the return value of the build method to obtain the mutated criteria object:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
def bartQuery = criteria.build {
    eq 'firstName', 'Bart'
}

A detached criteria also accepts sqlRestriction, which restricts the results with a native SQL condition as in a criteria query. {alias} stands for the table alias of the class of the detached criteria, also when the detached criteria is used as a subquery:

def criteria = new DetachedCriteria(Person).build {
    sqlRestriction "char_length({alias}.first_name) <= ?", [4]
}

30.7.2. Executing Detached Criteria Queries

Unlike regular criteria, Detached Criteria are lazy, in that no query is executed at the point of definition. Once a Detached Criteria query has been constructed then there are a number of useful query methods which are summarized in the table below:

Method Description

list

List all matching entities

get

Return a single matching result

count

Count all matching records

exists

Return true if any matching records exist

deleteAll

Delete all matching records

updateAll(Map)

Update all matching records with the given properties

As an example the following code will list the first 4 matching records sorted by the firstName property:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
def results = criteria.list(max:4, sort:"firstName")

You can also supply additional criteria to the list method:

def results = criteria.list(max:4, sort:"firstName") {
    gt 'age', 30
}

To retrieve a single result you can use the get or find methods (which are synonyms):

Person p = criteria.find() // or criteria.get()

The DetachedCriteria class itself also implements the Iterable interface which means that it can be treated like a list:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
criteria.each {
    println it.firstName
}

In this case the query is only executed when the each method is called. The same applies to all other Groovy collection iteration methods.

You can also execute dynamic finders on DetachedCriteria just like on domain classes. For example:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
def bart = criteria.findByFirstName("Bart")

30.7.3. Using Detached Criteria for Subqueries

Within the context of a regular criteria query you can use DetachedCriteria to execute subquery. For example if you want to find all people who are older than the average age the following query will accomplish that:

def results = Person.withCriteria {
     gt "age", new DetachedCriteria(Person).build {
         projections {
             avg "age"
         }
     }
     order "firstName"
 }

Notice that in this case the subquery class is the same as the original criteria query class (i.e. Person) and hence the query can be shortened to:

def results = Person.withCriteria {
     gt "age", {
         projections {
             avg "age"
         }
     }
     order "firstName"
 }

If the subquery class differs from the original criteria query then you will have to use the original syntax.

In the previous example the projection ensured that only a single result was returned (the average age). If your subquery returns multiple results then there are different criteria methods that need to be used to compare the result. For example to find all the people older than the ages 18 to 65 a gtAll query can be used:

def results = Person.withCriteria {
    gtAll "age", {
        projections {
            property "age"
        }
        between 'age', 18, 65
    }

    order "firstName"
}

The following table summarizes criteria methods for operating on subqueries that return multiple results:

Method Description

gtAll

greater than all subquery results

geAll

greater than or equal to all subquery results

ltAll

less than all subquery results

leAll

less than or equal to all subquery results

eqAll

equal to all subquery results

neAll

not equal to all subquery results

30.7.4. Batch Operations with Detached Criteria

The grails.gorm.DetachedCriteria class can be used to execute batch operations such as batch updates and deletes. For example, the following query will update all people with the surname "Simpson" to have the surname "Bloggs":

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
int total = criteria.updateAll(lastName:"Bloggs")
Note that one limitation with regards to batch operations is that join queries (queries that query associations) are not allowed within the DetachedCriteria instance. Neither is sqlRestriction: a batch operation on a detached criteria with a SQL restriction throws an exception.

To batch delete records you can use the deleteAll method:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
int total = criteria.deleteAll()

30.8. Hibernate Query Language (HQL)

31. HQL Queries

GORM supports querying using Hibernate Query Language (HQL), which is an object-oriented query language similar to SQL but operating on domain class names and properties rather than table and column names.

31.1. Basic HQL

The following static methods accept HQL strings:

  • find(CharSequence) — returns the first matching instance

  • findAll(CharSequence) — returns all matching instances

  • executeQuery(CharSequence) — returns a list (supports projections)

  • executeUpdate(CharSequence) — executes a bulk update/delete, returns the count

31.2. Safe Parameterization with GString

GORM automatically converts Groovy GString interpolations into safe named parameters before the query reaches Hibernate. This is the preferred way to pass user-supplied values.

String title = params.title  // user input

// ✅ SAFE — ${title} is extracted and bound as :p0
List results = Book.findAll("from Book b where b.title = ${title}")

// ✅ SAFE — multiple interpolations become :p0, :p1
List results = Book.findAll(
    "from Book b where b.title like ${title} and b.genre = ${genre}")
The single-argument overloads accept either a plain String (executed as written, exactly as on Hibernate 5) or a Groovy GString. A GString is never interpolated into the query text — every ${value} is extracted and bound as a named parameter — so the idiomatic interpolated form above is injection-safe by binding rather than escaping.
// ✅ SAFE — a static plain String contains no untrusted input
List results = Book.findAll("from Book where active = true")

// ✅ SAFE — the GString value is bound as :p0, never interpolated into the query
List results = Book.findAll("from Book where title = ${userInput}")

// ⚠️ UNSAFE — manually concatenating untrusted input into a plain String is an
// injection risk in any ORM; never build queries this way
String hql = "from Book where title = '" + userInput + "'"
Book.findAll(hql)

// ✅ Instead use the GString form above, or the parameterized overload
Book.findAll("from Book where title = :title", [title: userInput])

31.3. Named Parameters

Use the (CharSequence, Map) overload to pass named parameters explicitly. This also accepts a plain String, making it safe for dynamically constructed queries where GString syntax is inconvenient.

// Named parameters — safe with plain String
List results = Book.findAll(
    "from Book b where b.title = :title and b.author = :author",
    [title: params.title, author: params.author])

Book.executeUpdate(
    "update Book set active = :flag where genre = :genre",
    [flag: false, genre: 'Horror'])

31.4. Positional Parameters

// Positional parameters (?1, ?2, ...)
List results = Book.executeQuery(
    "from Book b where b.title like ?1 and b.genre = ?2",
    ['%Groovy%', 'Tech'])

31.5. Query Settings

find, findAll, and executeQuery overloads accept a settings map as the last argument. Use it for pagination and Hibernate query settings such as cache, readOnly, fetchSize, timeout, flushMode, and lock:

List results = Book.findAll(
    "from Book b where b.genre = ${genre} order by b.title",
    [max: 10, offset: 20])

List results = Book.executeQuery(
    "from Book b where b.genre = :genre",
    [genre: 'Tech'],
    [max: 5, offset: 0, cache: true])

List locked = Book.findAll(
    "from Book b where b.genre = :genre",
    [genre: 'Tech'],
    [lock: true])

When lock: true is used, GORM requests a pessimistic write lock and disables query caching for that query.

31.6. Bulk Updates and Deletes

// Bulk update with named params
int count = Book.executeUpdate(
    "update Book set active = :flag where publishedYear < :year",
    [flag: false, year: 2000])

// Bulk delete
int count = Book.executeUpdate(
    "delete Book where active = false")

31.7. Native SQL

32. SQL Queries

GORM provides findWithSql and findAllWithSql for executing raw SQL when HQL or the DetachedCriteria API cannot express the query you need (e.g. database-specific functions, complex joins, or legacy SQL).

Raw SQL bypasses Hibernate’s type system and object mapping. Prefer HQL, the DetachedCriteria API, or dynamic finders wherever possible. Use SQL only when there is no higher-level alternative.

32.1. Methods

Method Description

findWithSql(CharSequence sql)

Returns the first result mapped to the domain class

findWithSql(CharSequence sql, Map args)

Returns the first result; args controls pagination (max, offset, cache)

findAllWithSql(CharSequence sql)

Returns all results mapped to the domain class

findAllWithSql(CharSequence sql, Map args)

Returns all results; args controls pagination

32.2. Safe Usage - GString Value Parameters

When a query contains user-supplied values (not identifiers), use Groovy GString interpolation. GORM extracts each ${expression} and binds it as a named JDBC parameter, preventing injection.

String nameFilter = params.name  // user input

// SAFE - ${nameFilter} is bound as :p0, never inlined into the SQL string
List results = Club.findAllWithSql(
    "select * from club c where c.name like ${nameFilter} order by c.name")

32.3. Static SQL (No User Input)

A plain String constant with no user data is safe and accepted directly.

// SAFE - no user input, static SQL
List results = Club.findAllWithSql(
    "select * from club c order by c.name")

32.4. What Cannot Be Parameterized

SQL identifiers - table names, column names, schema names - cannot be bound as JDBC parameters. Do not interpolate them from user input under any circumstances.

// UNSAFE - table name from user input, cannot be made safe via GString
String table = params.table
Club.findAllWithSql("select * from ${table}")  // DO NOT DO THIS

// UNSAFE - string concatenation, no protection at all
Club.findAllWithSql("select * from club where name = '" + userInput + "'")

If you need dynamic identifiers (e.g. schema-per-tenant), use the JDBC identifier quoting API (connection.metaData.identifierQuoteString) to quote and sanitize the name before use - the same mechanism used internally by DefaultSchemaHandler.

33. Advanced GORM Features

34. Advanced GORM Features

This section covers advanced GORM and Hibernate mapping capabilities available through the static mapping {} DSL and other configuration mechanisms.

The ORM DSL mapping block is available on every domain class and allows you to customise every aspect of the Hibernate mapping:

class Book {
    String title
    Date dateCreated

    static mapping = {
        table     'books'
        title     column: 'book_title', index: true
        batchSize 20
        cache     usage: 'read-write'
    }
}

The following topics are covered in this section:

34.1. Events and Auto Timestamping

GORM supports the registration of events as methods that get fired when certain events occurs such as deletes, inserts and updates. The following is a list of supported events:

  • beforeInsert - Executed before an object is initially persisted to the database. If you return false, the insert will be cancelled.

  • beforeUpdate - Executed before an object is updated. If you return false, the update will be cancelled.

  • beforeDelete - Executed before an object is deleted. If you return false, the operation delete will be cancelled.

  • beforeValidate - Executed before an object is validated

  • afterInsert - Executed after an object is persisted to the database

  • afterUpdate - Executed after an object has been updated

  • afterDelete - Executed after an object has been deleted

  • onLoad - Executed when an object is loaded from the database

To add an event simply register the relevant method with your domain class.

Do not attempt to flush the session within an event (such as with obj.save(flush:true)). Since events are fired during flushing this will cause a StackOverflowError.

34.1.1. The beforeInsert event

Fired before an object is saved to the database

class Person {
   private static final Date NULL_DATE = new Date(0)

   String firstName
   String lastName
   Date signupDate = NULL_DATE

   def beforeInsert() {
      if (signupDate == NULL_DATE) {
         signupDate = new Date()
      }
   }
}

34.1.2. The beforeUpdate event

Fired before an existing object is updated

class Person {

   def securityService

   String firstName
   String lastName
   String lastUpdatedBy

   static constraints = {
      lastUpdatedBy nullable: true
   }

   static mapping = {
      autowire true
   }

   def beforeUpdate() {
      lastUpdatedBy = securityService.currentAuthenticatedUsername()
   }
}

Notice the usage of autowire true above. This is required for the bean securityService to be injected.

34.1.3. The beforeDelete event

Fired before an object is deleted.

class Person {
   String name

   def beforeDelete() {
      ActivityTrace.withNewSession {
         new ActivityTrace(eventName: "Person Deleted", data: name).save()
      }
   }
}

Notice the usage of withNewSession method above. Since events are triggered whilst Hibernate is flushing using persistence methods like save() and delete() won’t result in objects being saved unless you run your operations with a new Session.

Fortunately the withNewSession method lets you share the same transactional JDBC connection even though you’re using a different underlying Session.

34.1.4. The beforeValidate event

Fired before an object is validated.

class Person {
   String name

   static constraints = {
       name size: 5..45
   }

   def beforeValidate() {
       name = name?.trim()
   }
}

The beforeValidate method is run before any validators are run.

Validation may run more often than you think. It is triggered by the validate() and save() methods as you’d expect, but it is also typically triggered just before the view is rendered as well. So when writing beforeValidate() implementations, make sure that they can handle being called multiple times with the same property values.

GORM supports an overloaded version of beforeValidate which accepts a List parameter which may include the names of the properties which are about to be validated. This version of beforeValidate will be called when the validate method has been invoked and passed a List of property names as an argument.

class Person {
   String name
   String town
   Integer age

   static constraints = {
       name size: 5..45
       age range: 4..99
   }

   def beforeValidate(List propertiesBeingValidated) {
      // do pre validation work based on propertiesBeingValidated
   }
}

def p = new Person(name: 'Jacob Brown', age: 10)
p.validate(['age', 'name'])
Note that when validate is triggered indirectly because of a call to the save method that the validate method is being invoked with no arguments, not a List that includes all of the property names.

Either or both versions of beforeValidate may be defined in a domain class. GORM will prefer the List version if a List is passed to validate but will fall back on the no-arg version if the List version does not exist. Likewise, GORM will prefer the no-arg version if no arguments are passed to validate but will fall back on the List version if the no-arg version does not exist. In that case, null is passed to beforeValidate.

34.1.5. The onLoad/beforeLoad event

Fired immediately before an object is loaded from the database:

class Person {
   String name
   Date dateCreated
   Date lastUpdated

   def onLoad() {
      log.debug "Loading ${id}"
   }
}

beforeLoad() is effectively a synonym for onLoad(), so only declare one or the other.

34.1.6. The afterLoad event

Fired immediately after an object is loaded from the database:

class Person {
   String name
   Date dateCreated
   Date lastUpdated

   def afterLoad() {
      name = "I'm loaded"
   }
}

34.1.7. Custom Event Listeners

To register a custom event listener you need to subclass AbstractPersistenceEventListener (in package org.grails.datastore.mapping.engine.event) and implement the methods onPersistenceEvent and supportsEventType. You also must provide a reference to the datastore to the listener. The simplest possible implementation can be seen below:

public MyPersistenceListener(final Datastore datastore) {
    super(datastore)
}

@Override
protected void onPersistenceEvent(final AbstractPersistenceEvent event) {
    switch(event.eventType) {
        case PreInsert:
            println "PRE INSERT \${event.entityObject}"
        break
        case PostInsert:
            println "POST INSERT \${event.entityObject}"
        break
        case PreUpdate:
            println "PRE UPDATE \${event.entityObject}"
        break;
        case PostUpdate:
            println "POST UPDATE \${event.entityObject}"
        break;
        case PreDelete:
            println "PRE DELETE \${event.entityObject}"
        break;
        case PostDelete:
            println "POST DELETE \${event.entityObject}"
        break;
        case PreLoad:
            println "PRE LOAD \${event.entityObject}"
        break;
        case PostLoad:
            println "POST LOAD \${event.entityObject}"
        break;
    }
}

@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
    return true
}

The AbstractPersistenceEvent class has many subclasses (PreInsertEvent, PostInsertEvent etc.) that provide further information specific to the event. A cancel() method is also provided on the event which allows you to veto an insert, update or delete operation.

GORM for Hibernate 7 also publishes a PersistEvent (eventType Persist) whenever an entity is persisted, whether explicitly through save() or by a cascade when the session flushes, and a MergeEvent (eventType Merge) whenever an entity is merged. Both are published before Hibernate performs the operation and cannot be cancelled; the exception is an application that registers a listener of its own for merge or persist, in which case that listener performs the operation and GORM’s event is published after it. A MergeEvent carries the detached instance passed to the merge rather than the managed copy it produces, and neither event is published for an uninitialized proxy.

GORM for Hibernate 5 publishes a SaveOrUpdateEvent (eventType SaveOrUpdate) when an entity is saved, and publishes no event for a merge. A listener that switches over every eventType and runs against either version should therefore handle all three values.

Once you have created your event listener you need to register it. If you are using Spring this can be done via the ApplicationContext:

HibernateDatastore datastore = applicationContext.getBean(HibernateDatastore)
applicationContext.addApplicationListener new MyPersistenceListener(datastore)

If you are not using Spring then you can register the event listener using the getApplicationEventPublisher() method:

HibernateDatastore datastore = ... // get a reference to the datastore
datastore.getApplicationEventPublisher()
         .addApplicationListener new MyPersistenceListener(datastore)

34.1.8. Hibernate Events

It is generally encouraged to use the non-Hibernate specific API described above, but if you need access to more detailed Hibernate events then you can define custom Hibernate-specific event listeners.

You can also register event handler classes in an application’s grails-app/conf/spring/resources.groovy or in the doWithSpring closure in a plugin descriptor by registering a Spring bean named hibernateEventListeners. This bean has one property, listenerMap which specifies the listeners to register for various Hibernate events.

The values of the Map are instances of classes that implement one or more Hibernate listener interfaces. You can use one class that implements all of the required interfaces, or one concrete class per interface, or any combination. The valid Map keys and corresponding interfaces are listed here:

Name Interface

auto-flush

AutoFlushEventListener

merge

MergeEventListener

create

PersistEventListener

create-onflush

PersistEventListener

delete

DeleteEventListener

dirty-check

DirtyCheckEventListener

evict

EvictEventListener

flush

FlushEventListener

flush-entity

FlushEntityEventListener

load

LoadEventListener

load-collection

InitializeCollectionEventListener

lock

LockEventListener

refresh

RefreshEventListener

replicate

ReplicateEventListener

save-update

SaveOrUpdateEventListener

save

SaveOrUpdateEventListener

update

SaveOrUpdateEventListener

pre-load

PreLoadEventListener

pre-update

PreUpdateEventListener

pre-delete

PreDeleteEventListener

pre-insert

PreInsertEventListener

pre-collection-recreate

PreCollectionRecreateEventListener

pre-collection-remove

PreCollectionRemoveEventListener

pre-collection-update

PreCollectionUpdateEventListener

post-load

PostLoadEventListener

post-update

PostUpdateEventListener

post-delete

PostDeleteEventListener

post-insert

PostInsertEventListener

post-commit-update

PostUpdateEventListener

post-commit-delete

PostDeleteEventListener

post-commit-insert

PostInsertEventListener

post-collection-recreate

PostCollectionRecreateEventListener

post-collection-remove

PostCollectionRemoveEventListener

post-collection-update

PostCollectionUpdateEventListener

For example, you could register a class AuditEventListener which implements PostInsertEventListener, PostUpdateEventListener, and PostDeleteEventListener using the following in an application:

beans = {

   auditListener(AuditEventListener)

   hibernateEventListeners(HibernateEventListeners) {
      listenerMap = ['post-insert': auditListener,
                     'post-update': auditListener,
                     'post-delete': auditListener]
   }
}

or use this in a plugin:

def doWithSpring = {

   auditListener(AuditEventListener)

   hibernateEventListeners(HibernateEventListeners) {
      listenerMap = ['post-insert': auditListener,
                     'post-update': auditListener,
                     'post-delete': auditListener]
   }
}

34.1.9. Automatic timestamping

If you define a dateCreated property it will be set to the current date for you when you create new instances. Likewise, if you define a lastUpdated property it will be automatically be updated for you when you change persistent instances.

If this is not the behaviour you want you can disable this feature with:

class Person {
   Date dateCreated
   Date lastUpdated
   static mapping = {
      autoTimestamp false
   }
}
If you have nullable: false constraints on either dateCreated or lastUpdated, your domain instances will fail validation - probably not what you want. Omit constraints from these properties unless you disable automatic timestamping.

It is also possible to disable the automatic timestamping temporarily. This is most typically done in the case of a test where you need to define values for the dateCreated or lastUpdated in the past. It may also be useful for importing old data from other systems where you would like to keep the current values of the timestamps.

Timestamps can be temporarily disabled for all domains, a specified list of domains, or a single domain. To get started, you need to get a reference to the AutoTimestampEventListener. If you already have access to the datastore, you can execute the getAutoTimestampEventListener method. If you don’t have access to the datastore, inject the autoTimestampEventListener bean.

Once you have a reference to the event listener, you can execute withoutDateCreated, withoutLastUpdated, or withoutTimestamps. The withoutTimestamps method will temporarily disable both dateCreated and lastUpdated.

Example:

//Only the dateCreated property handling will be disabled for only the Foo domain
autoTimestampEventListener.withoutDateCreated(Foo) {
    new Foo(dateCreated: new Date() - 1).save(flush: true)
}

//Only the lastUpdated property handling will be disabled for only the Foo and Bar domains
autoTimestampEventListener.withoutLastUpdated(Foo, Bar) {
    new Foo(lastUpdated: new Date() - 1, bar: new Bar(lastUpdated: new Date() + 1)).save(flush: true)
}

//All timestamp property handling will be disabled for all domains
autoTimestampEventListener.withoutTimestamps {
    new Foo(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true)
    new Bar(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true)
    new FooBar(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true)
}
Because the timestamp handling is only disabled for the duration of the closure, you must flush the session during the closure execution!
The timestamp handling is only disabled for the thread executing the closure. Other threads persisting entities at the same time are unaffected and will continue to have their timestamps applied.

If work inside the closure runs on other threads (for example an executor-based import), the suppression must be propagated to those threads explicitly. Capture the state with captureTimestampSuppression and apply it on the executing thread with withTimestampSuppression:

autoTimestampEventListener.withoutTimestamps {
    def suppression = autoTimestampEventListener.captureTimestampSuppression()
    executor.submit {
        autoTimestampEventListener.withTimestampSuppression(suppression) {
            new Foo(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true)
        }
    }.get()
}

The captured state is immutable and may be applied on any number of threads, even after the capturing closure has completed.

34.2. Custom ORM Mapping

35. ORM DSL

The ORM DSL is a static mapping closure on every domain class that gives you fine-grained control over how GORM maps your domain model to the database schema.

See the subsections below for details on each feature:

35.1. Table and Column Names

36. Table and Column Names

By default GORM derives table and column names from your domain class and property names using an underscore-based naming strategy. You can override these defaults with the ORM DSL mapping block.

36.1. Changing the Table Name

class Book {
    String title
    static mapping = {
        table 'books'           (1)
    }
}
1 Maps the Book domain class to a table named books.

You can also specify a catalog and/or schema:

class Book {
    String title
    static mapping = {
        table name: 'books', catalog: 'inventory', schema: 'dbo'
    }
}

36.2. Changing Column Names

Use the property name followed by column to override the column name for any property:

class Book {
    String title
    static mapping = {
        title column: 'book_title'
    }
}

For multi-column user types, call column multiple times:

class Payment {
    Money amount
    static mapping = {
        amount {
            column name: 'amount_value'
            column name: 'amount_currency'
        }
    }
}

36.3. Column Properties

The column block supports the following attributes:

attribute description default

name

The column name

derived from property name

sqlType

The SQL type override

derived from Hibernate type

unique

Whether the column has a unique constraint

false

index

Index name (or true for an auto-named index)

none

defaultValue

The DDL default value for the column

none

comment

A DDL comment for the column

none

read

A SQL expression to use when reading the value

none

write

A SQL expression to use when writing the value

none

36.4. Join Table Configuration for Collections

When a domain class has a hasMany relationship without a belongsTo on the other side (unidirectional), or for hasMany of basic/enum types, GORM uses a join table. You can customise the join table name and its columns:

class Author {
    static hasMany = [books: Book]
    static mapping = {
        books joinTable: 'author_books'     (1)
    }
}
1 Override the join table name only.
class Author {
    static hasMany = [books: Book]
    static mapping = {
        books joinTable: [
            name: 'author_books',       (1)
            key: 'author_fk',           (2)
            column: 'book_fk'           (3)
        ]
    }
}
1 The join table name.
2 The foreign-key column that points back to Author.
3 The foreign-key column that points to Book (or holds the element value for basic types).

You can also use the closure form:

class Author {
    static hasMany = [books: Book]
    static mapping = {
        books joinTable {
            name 'author_books'
            key  'author_fk'
            column 'book_fk'
        }
    }
}

36.4.1. Caching Strategy

37. Caching

GORM supports Hibernate’s second-level cache and query cache. Caching is configured per domain class and optionally per association.

37.1. Enabling Second-Level Cache

Second-level cache is enabled globally via configuration:

hibernate:
    use_second_level_cache: true
    cache:
        region:
            factory_class: 'org.hibernate.cache.jcache.internal.JCacheRegionFactory'

Then enable caching for individual domain classes using the cache directive in the mapping block:

class Book {
    String title
    static mapping = {
        cache true          (1)
    }
}
1 Enables the second-level cache for Book with the default read-write usage.

37.2. Cache Usage

You can control the cache usage strategy:

static mapping = {
    cache usage: 'read-only'    (1)
}
usage description

read-write

Cached data can be read and written — default

read-only

Cached data is never modified (best performance for immutable data)

nonstrict-read-write

No strict locking; possible stale reads between updates

transactional

Full transaction support (requires a JTA transaction manager)

37.3. Cache Include

The include option controls what data to cache:

static mapping = {
    cache usage: 'read-write', include: 'non-lazy'  (1)
}
1 Only non-lazy properties are cached. Use all (default) to include lazy properties too.

37.4. Caching Associations

You can cache collection associations independently:

class Author {
    String name
    static hasMany = [books: Book]
    static mapping = {
        books cache: true
    }
}

37.5. Query Cache

To cache the results of individual queries, pass cache: true in the query options and enable the query cache globally:

hibernate:
    cache:
        use_query_cache: true
List<Book> books = Book.findAllByGenre('Fiction', [cache: true])
Only use the query cache for queries whose results change infrequently. Cached queries are invalidated whenever any entity in the queried table is updated.

37.5.1. Inheritance Strategies

38. Inheritance Strategies

GORM supports three Hibernate inheritance mapping strategies.

38.1. Table-per-Hierarchy (Default)

All classes in the hierarchy are stored in a single table. A discriminator column distinguishes rows for each subclass. This is the default:

class Content {
    String title
    // tablePerHierarchy is true by default
}

class BlogPost extends Content {
    String body
}

class Page extends Content {
    String html
}

The single table will contain columns for all properties of all subclasses, with nullable columns for subclass-specific fields.

By default the discriminator column is named class and each row stores the fully qualified name of its class, for example com.example.Content or com.example.BlogPost. This is the same default as grails-data-hibernate5, so existing table-per-hierarchy tables can be used without a mapping change. Any class that does not set a discriminator value keeps its fully qualified class name, including when the root class maps only the discriminator column:

class Content {
    String title
    static mapping = {
        discriminator column: 'content_type'    // values remain the fully qualified class names
    }
}

38.1.1. Customising the Discriminator

class Content {
    String title
    static mapping = {
        discriminator column: 'content_type', value: 'content'
    }
}

class BlogPost extends Content {
    static mapping = {
        discriminator 'blog'    (1)
    }
}
1 The discriminator value stored for BlogPost rows.

You can also configure the discriminator column type and whether it is insertable:

static mapping = {
    discriminator {
        column name: 'content_type', sqlType: 'varchar(30)'
        value  'blog'
        insert false
    }
}

38.2. Table-per-Subclass

Each subclass has its own table containing only the subclass-specific columns, joined to the parent table via a foreign key:

class Content {
    String title
    static mapping = {
        tablePerHierarchy false     (1)
    }
}

class BlogPost extends Content {
    String body
    // implicitly uses joined-subclass mapping
}
1 Disables single-table strategy; Hibernate will use joined subclass tables.

38.3. Table-per-Concrete-Class

Each concrete class has its own standalone table with all columns (inherited + its own). There is no shared parent table:

class Content {
    String title
    static mapping = {
        tablePerConcreteClass true  (1)
    }
}
1 Each concrete subclass gets its own fully self-contained table.
Table-per-hierarchy is the most performant strategy because it requires no joins. Table-per-subclass is useful when you need to query on a subclass without nulls in the parent table. Table-per-concrete-class is least commonly used and makes polymorphic queries expensive.

38.3.1. Custom Database Identity

39. Identity

GORM automatically adds an id property and a version property to every domain class. The mapping block lets you customise both.

39.1. Generator Strategy

The default generator is native, which delegates to the database for id generation (auto-increment, sequences, etc.). You can change this globally or per-class:

class Book {
    String title
    static mapping = {
        id generator: 'sequence', params: [sequence_name: 'book_seq']   (1)
    }
}
1 Uses a named database sequence for the id column.

Common generator values:

value description

native

Delegates to the database (auto-increment / sequence) — default

assigned

Application assigns the id before saving

uuid

Generates a UUID string id

sequence

Uses a named database sequence (configure via params)

increment

GORM-managed incrementing long — not suitable for clusters

identity

Database IDENTITY / auto-increment column

39.2. Column Name

static mapping = {
    id column: 'book_id'
}

39.3. Composite Identifiers

39.4. Disabling Auto-generated Version

The version column enables optimistic locking. To disable it:

static mapping = {
    version false
}

You can also map it to a different column:

static mapping = {
    version column: 'revision'
}

39.4.1. Composite Primary Keys

40. Composite Primary Keys

GORM allows you to map domain classes to tables that use a composite primary key (a key composed of two or more columns). This is commonly needed when mapping to legacy database schemas.

40.1. Defining a Composite Identity

Use id composite: [...] in the mapping block, listing the property names that together form the primary key:

class OrderItem {
    Long orderId
    Long productId
    Integer quantity

    static mapping = {
        id composite: ['orderId', 'productId']  (1)
    }
}
1 The composite key is made up of orderId and productId.
Domain classes with composite keys do not have the auto-generated id and version properties. You are responsible for setting the key properties before calling save().

40.2. Using Composite Keys

def item = new OrderItem(orderId: 1L, productId: 42L, quantity: 3)
item.save()

// Load by composite key — pass a map
def found = OrderItem.get(orderId: 1L, productId: 42L)

40.3. Associations with Composite Keys

When another domain class references a domain class with a composite key, GORM creates multiple foreign-key columns automatically:

class OrderLine {
    Integer lineNumber
    OrderItem item     // foreign key will use both orderId and productId columns
}
Composite primary keys add complexity to all queries and associations. Prefer surrogate (auto-generated) single-column keys wherever possible and use composite unique constraints instead of composite keys for business-key uniqueness requirements.

40.3.1. Database Indices

41. Database Indices

GORM lets you define database indices on domain class columns directly in the mapping block, so they are created automatically when hbm2ddl generates the schema.

41.1. Single-Column Index

Set index: true on a column to create an auto-named index, or provide a string name to name it explicitly:

class Book {
    String title
    String isbn
    static mapping = {
        title  index: true          (1)
        isbn   index: 'isbn_idx'    (2)
    }
}
1 Creates an unnamed (auto-named) index on title.
2 Creates a named index isbn_idx on isbn.

41.2. Composite Index

To create a composite index across multiple columns, use the same index name on each column:

class OrderItem {
    Long orderId
    Long productId
    static mapping = {
        orderId   index: 'order_product_idx'    (1)
        productId index: 'order_product_idx'    (1)
    }
}
1 Both columns share the same index name, so Hibernate creates a single composite index.

41.3. Unique Index

You can combine index with unique to create a unique index:

class Book {
    String isbn
    static mapping = {
        isbn unique: true, index: 'isbn_unique_idx'
    }
}
Indices are only created automatically when hibernate.hbm2ddl.auto is set to create, create-drop, or update. For production schemas, prefer explicit DDL migration scripts.

41.3.1. Optimistic Locking and Versioning

42. Optimistic Locking and Versioning

GORM enables optimistic locking by default via a version column added to every domain class table.

42.1. How Optimistic Locking Works

When you call save(), Hibernate checks that the version in the database matches the version loaded by the current session. If another transaction modified the row in between, the versions will differ and Hibernate throws StaleObjectStateException:

def book = Book.get(1)
// ... another thread or transaction updates the same book row ...
book.title = "New Title"
book.save()  // throws StaleObjectStateException if version was incremented elsewhere

Handle this with a try/catch in your service or controller:

try {
    book.save(failOnError: true)
} catch (org.hibernate.StaleObjectStateException e) {
    // handle conflict: reload and retry, or inform the user
}

42.2. Disabling Optimistic Locking

class Book {
    String title
    static mapping = {
        version false   (1)
    }
}
1 No version column is created; concurrent modifications are not detected.
Disabling versioning removes all optimistic locking protection. Concurrent updates to the same row will silently overwrite each other.

42.3. Customising the Version Column

static mapping = {
    version column: 'revision'
}

42.4. Locking Pessimistically

For cases where you need a database-level lock, use GORM’s lock() method:

Book.withTransaction {
    def book = Book.lock(1)   (1)
    book.title = "Locked Update"
    book.save()
}
1 Issues a SELECT ... FOR UPDATE. Conflicting writes and lock requests block until the transaction commits; ordinary reads are not necessarily blocked.

If the entity is already loaded in the current session, Book.lock(1) locks and version-checks the loaded state rather than reloading it. Pass refresh: true (Book.lock(1, refresh: true)) or call book.refresh(lock: true) to reload its state and version under the lock instead. See Optimistic and Pessimistic Locking for details.

42.4.1. Eager and Lazy Fetching

43. Fetching Strategies

GORM supports both lazy (default) and eager fetching for associations. You can control this per-property via the ORM DSL mapping block.

43.1. Lazy Fetching (Default)

By default, associations are loaded lazily — Hibernate issues a secondary query only when you first access the association:

class Author {
    String name
    static hasMany = [books: Book]
    // books are loaded lazily by default
}

43.2. Eager Fetching

To eagerly load an association in the same query as the owning entity, use fetch: 'join':

class Author {
    String name
    static hasMany = [books: Book]
    static mapping = {
        books fetch: 'join'     (1)
    }
}
1 Hibernate uses a SQL JOIN to load books alongside the Author.

You can also use fetch: 'select' to trigger a secondary SELECT eagerly (as opposed to lazy, which defers the select until access):

static mapping = {
    books fetch: 'select'   // loads eagerly via a secondary SELECT
}

43.3. Batch Fetching

Batch fetching is a performance optimisation that allows Hibernate to initialise multiple lazy proxies or collections in a single SELECT. Configure it with batchSize:

class Author {
    String name
    static hasMany = [books: Book]
    static mapping = {
        books batchSize: 10     (1)
    }
}
1 When accessing books on an uninitialized proxy, Hibernate will fetch up to 10 collections in one query.

Batch size can also be set at the class level, which affects all lazy-loaded instances of that class:

class Book {
    String title
    static mapping = {
        batchSize 10
    }
}

43.4. Lazy vs. Eager — Recommendations

Eager fetching avoids N+1 query problems but can return large result sets. Prefer lazy loading with explicit eager overrides (via named queries or where clauses with .join()) for fine-grained control.

43.4.1. Custom Cascade Behaviour

44. Custom Cascade Behaviour

Hibernate cascades control which persistence operations (save, update, delete, etc.) are automatically propagated from a parent entity to its associated children.

44.1. Default Cascade Behaviour

By default GORM applies save-update cascading on associations — when you save or update an entity, changes to its associated objects are also persisted. Deletions are not cascaded by default.

44.2. Configuring Cascade

Use the cascade option in the mapping block to override the default:

class Author {
    String name
    static hasMany = [books: Book]
    static mapping = {
        books cascade: 'all'        (1)
    }
}
1 all cascades all operations including delete to books.
value description

all

Propagates all operations (save/update/delete/merge/refresh)

save-update

Propagates save and update — default

delete

Propagates delete only

all-delete-orphan

Like all but also deletes child rows not present in the collection

none

No cascade

44.3. Cascade Delete Example

class Author {
    String name
    static hasMany = [books: Book]
    static mapping = {
        books cascade: 'all-delete-orphan'  (1)
    }
}
1 When you remove a Book from author.books and call author.save(), the removed Book row is also deleted from the database.
Use all-delete-orphan when the child entity has no meaning outside the parent (composition). Use save-update (default) when the child may belong to multiple parents (aggregation).

44.3.1. Custom Hibernate Types

45. Custom Hibernate Types

GORM allows you to map properties to custom Hibernate UserType implementations. This is useful for persisting non-standard Java/Groovy types — for example, storing a List as a comma-separated string, or encrypting values at the persistence layer.

45.1. Per-Property Type

Use the type option in the mapping block to assign a custom Hibernate type to a specific property:

class Setting {
    String name
    Serializable value

    static mapping = {
        value type: 'serializable'      (1)
    }
}
1 The type name can be a Hibernate built-in type alias, a fully qualified class name, or a Class object.

With a custom UserType class:

class Product {
    String name
    List<String> tags

    static mapping = {
        tags type: CsvStringListType     (1)
    }
}
1 CsvStringListType implements org.hibernate.usertype.UserType and handles conversion between a comma-separated column value and a List<String>.

45.2. Type Parameters

If your custom type implements org.hibernate.usertype.ParameterizedType, pass parameters using typeParams:

class Measurement {
    BigDecimal value

    static mapping = {
        value type: FixedScaleDecimalType, typeParams: [scale: '4']
    }
}

45.3. Global User Type Mapping

Use user-type to register a user type for a Java type, so that every property of that type uses it without a per-property type. Registered in a domain class’s mapping block, it applies to the properties of that class:

class Account {
    Boolean active
    Boolean locked

    static mapping = {
        'user-type'(type: YesNoBooleanType, class: Boolean)    (1)
    }
}
1 YesNoBooleanType implements org.hibernate.usertype.UserType<Boolean> and stores booleans as Y or N. Both active and locked use it. The type can be the user type’s Class or its fully qualified class name.

To apply it to every domain class, register it in the default mapping:

grails-app/conf/application.groovy
grails.gorm.default.mapping = {
    'user-type'(type: YesNoBooleanType, class: Boolean)
}

A type set on an individual property takes precedence over a registered user type.

A Hibernate 7 UserType must implement getSqlType(), returnedClass(), deepCopy(), and isMutable(), and usually overrides nullSafeGet() and nullSafeSet() to convert between the column value and the Java type. Prefer Hibernate’s CompositeUserType for multi-column mappings.

45.3.1. Derived Properties

46. Derived Properties

A derived property is a read-only property whose value is computed by a SQL formula rather than stored in a dedicated column.

46.1. Defining a Derived Property

Use the formula option in the mapping block:

class Order {
    BigDecimal subtotal
    BigDecimal taxRate

    BigDecimal tax                  (1)
    BigDecimal total                (1)

    static mapping = {
        tax   formula: 'subtotal * tax_rate'            (2)
        total formula: 'subtotal + (subtotal * tax_rate)'
    }
}
1 tax and total have no corresponding columns in the table.
2 The formula is a raw SQL expression evaluated by the database.

46.2. Reading Derived Values

Derived properties are populated when an entity is loaded:

def order = Order.get(1)
println order.tax   // value computed by the database formula
Derived properties are read-only. Setting them in Groovy code does not affect the database value — the formula always takes precedence when reloading.

46.3. Column-Level Formulas (Read/Write Expressions)

For finer control over individual column values, use read and write expressions on a regular property:

class CreditCard {
    String cardNumber
    static mapping = {
        cardNumber {
            read  "decrypt(card_number)"    (1)
            write "encrypt(?)"              (2)
        }
    }
}
1 SQL expression used when reading the column value.
2 SQL expression wrapping the bound parameter when writing.

46.3.1. Custom Naming Strategy

47. Custom Naming Strategy

By default GORM uses Hibernate’s snake_case physical naming strategy (PhysicalNamingStrategySnakeCaseImpl), which converts camelCase class and property names to snake_case table and column names (e.g., BookAuthor → book_author, firstName → first_name).

47.1. Configuring a Custom Strategy

You can replace this with any Hibernate PhysicalNamingStrategy implementation via application configuration:

hibernate:
    physicalNamingStrategy: com.example.MyCustomNamingStrategy

Or set it per datasource:

dataSources:
    reporting:
        hibernate:
            physicalNamingStrategy: com.example.LegacyNamingStrategy

47.2. Implementing a Custom Strategy

Implement Hibernate’s org.hibernate.boot.model.naming.PhysicalNamingStrategy interface:

import org.hibernate.boot.model.naming.Identifier
import org.hibernate.boot.model.naming.PhysicalNamingStrategy
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment

class UpperCaseNamingStrategy implements PhysicalNamingStrategy {

    @Override
    Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) {
        return Identifier.toIdentifier(name.text.toUpperCase())
    }

    @Override
    Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) {
        return Identifier.toIdentifier(name.text.toUpperCase())
    }

    // ... other required method overrides ...
}
Individual column or table names set explicitly in the mapping block always take precedence over what the naming strategy would produce.

For a unidirectional hasMany (a collection with no belongsTo or reciprocal hasMany on the other side), the default foreign-key column that references the associated entity is derived from that entity’s physical table name. Consequently, a custom strategy that changes a domain table name also changes that column. For example, given static hasMany = [books: TBook], if the strategy maps TBook to the table book, the default foreign-key column is book_id, not tbook_id.

This does not apply to a bidirectional many-to-many association: both of its join-table foreign-key columns are still derived from the class names, regardless of any table mapping or naming strategy. Applications upgrading from an earlier GORM version should account for the unidirectional case’s schema change, or configure the join-table columns explicitly in the mapping block. See Join-Table Foreign-Key Column Names in the upgrade guide for details and a migration example.

47.3. Default Sort Order

48. Default Sort Order

You can configure a default sort order for list() and findAll* queries at the domain class level using the mapping block:

class Book {
    String title
    Date dateCreated

    static mapping = {
        sort 'title'            (1)
    }
}
1 All Book.list() calls will return results sorted by title in ascending order by default.

48.1. Sort Direction

static mapping = {
    sort title: 'desc'          (1)
}
1 Sort by title descending.

48.2. Default Sort on Associations

You can also define a default sort order on a hasMany collection:

class Author {
    static hasMany = [books: Book]
    static mapping = {
        books sort: 'title', order: 'asc'
    }
}
The default sort order in mapping applies to queries that do not specify their own order. Any query that specifies sort or order explicitly will override the default.

49. Programmatic Transactions

50. Programmatic Transactions

GORM integrates with Spring’s transaction management. All persistence operations should run within a transaction.

50.1. withTransaction

Use withTransaction on any domain class to run a block within a transaction:

Book.withTransaction {
    new Book(title: 'Grails in Action', author: 'Glen Smith').save()
    new Book(title: 'Groovy in Action', author: 'Dierk König').save()
    // both are committed together; any exception rolls back both
}

The closure receives a TransactionStatus parameter if needed:

Book.withTransaction { TransactionStatus status ->
    def book = new Book(title: 'Test')
    book.save()
    if (someCondition) {
        status.setRollbackOnly()    (1)
    }
}
1 Marks the transaction for rollback without throwing an exception.

50.2. withNewTransaction

Start a new, independent transaction (suspending the current one if any):

Book.withNewTransaction {
    // runs in a brand-new transaction regardless of any outer transaction
}

50.3. withSession

Access the underlying Hibernate Session directly:

Book.withSession { session ->
    session.flush()
    session.clear()     (1)
}
1 Evicts all entities from the first-level cache.

50.4. Service-Layer Transactions

In a Grails application, services are transactional by default. Annotate individual methods or the entire service class with Spring’s @Transactional for fine-grained control:

import org.springframework.transaction.annotation.Transactional

@Transactional
class BookService {
    def transferOwnership(Long bookId, Long newAuthorId) {
        def book = Book.get(bookId)
        book.author = Author.get(newAuthorId)
        book.save(failOnError: true)
    }
}

51. GORM Data Services

Introduced in GORM 6.1, Data Services take the work out of implemented service layer logic by adding the ability to automatically implement abstract classes or interfaces using GORM logic.

To illustrate what GORM Data Services are about let’s walk through an example.

51.1. Data Service Basics

51.1.1. Writing a Simple Data Service

In a Grails application you can create a Data Service in either src/main/groovy or grails-app/services. To write a Data Service you should create either an interface (although abstract classes can also be used, more about that later) and annotate it with the grails.gorm.services.Service annotation with the domain class the service applies to:

@Service(Book)
interface BookService {
    Book getBook(Serializable id)
}

The @Service annotation is an AST transformation that will automatically implement the service for you. You can then obtain the service via Spring autowiring:

@Autowired BookService bookService

Or if you are using GORM standalone by looking it up from the HibernateDatastore instance:

BookService bookService = hibernateDatastore.getService(BookService)
The above example also works in Spock unit tests that extend HibernateSpec

51.1.2. How Does it Work?

The @Service transformation will look at the the method signatures of the interface and make a best effort to find a way to implement each method.

If a method cannot be implemented then a compilation error will occur. At this point you have the option to use an abstract class instead and provide an implementation yourself.

The @Service transformation will also generate a META-INF/services file for the service so it can be discovered via the standard Java service loader. So no additional configuration is necessary.

51.1.3. Advantages of Data Services

There are several advantages to Data Services that make them worth considering to abstract your persistence logic.

  • Type Safety - Data service method signatures are compile time checked and compilation will fail if the types of any parameters don’t match up with properties in your domain class

  • Testing - Since Data Services are interfaces this makes them easy to test via Spock Mocks

  • Performance - The generated services are statically compiled and unlike competing technologies in the Java space no proxies are created so runtime performance doesn’t suffer

  • Transaction Management - Each method in a Data Service is wrapped in an appropriate transaction (a read-only transaction in the case of read operations) that can be easily overridden.

51.1.4. Abstract Class Support

If you come across a method that GORM doesn’t know how to implement, then you can provide an implementation by using an abstract class.

For example:

interface IBookService {
    Book getBook(Serializable id)
    Date someOtherMethod()
}
@Service(Book)
abstract class BookService implements IBookService {

   @Override
   Date someOtherMethod() {
      // impl
   }
}

In this case GORM will implement the interface methods that have not been defined by the abstract class.

In addition, all public methods of the domain class will be automatically wrapped in the appropriate transaction handling.

What this means is that you can define protected abstract methods that are non-transactional in order to compose logic. For example:

@Service(Book)
abstract class BookService  {

   protected abstract Book getBook(Serializable id) (1)

   protected abstract Author getAuthor(Serializable id) (1)

   Book updateBook(Serializable id, Serializable authorId) { (2)
      Book book = getBook(id)
      if(book != null) {
          Author author = getAuthor(authorId)
          if(author == null) {
              throw new IllegalArgumentException("Author does not exist")
          }
          book.author = author
          book.save()
      }
      return book
   }
}
1 Two protected abstract methods are defined that are not wrapped in transaction handling
2 The updateBook method uses the two methods that are implemented automatically by GORM and being public is automatically made transactional.
If you have public methods that you do not wish to be transactional, then you can annotate them with @NotTransactional

51.2. Data Service Queries

GORM Data Services will implement queries for you using a number of different strategies and conventions.

It does this by looking at the return type of a method and the method stem and picking the most appropriate implementation.

The following table summarizes the conventions:

Table 1. Data Service Conventions
Method Stem Description Possible Return Types

count*

Count the number of results

Subclass of Number or Observable<Number>

countBy*

Dynamic Finder Count the number of results

Subclass of Number or Observable<Number>

delete*

Delete an instance for the given arguments

T, void, subclass of Number or Observable<Number>

find*, get*, list* or retrieve*

Query for the given parameters

T, Iterable<T>, T[], List<T>, Observable<T>

findBy*, listBy*, findAllBy* or getBy*

Dynamic finder query for given parameters

T, Iterable<T>, T[], List<T>, Observable<T>

save*, store*, or persist*

Save a new instance

T or Observable<T>

update*

Updates an existing instance. First parameter should be id

T or Observable<T>

The conventions are extensible (more on that later), in terms of queries there are two distinct types.

51.2.1. Simple Queries

Simple queries are queries that use the arguments of the method. For example:

@Service(Book)
interface BookService {
    Book findBook(String title)
}

In the example above the Data Service will generate the implementation based on the fact that the title parameter matches the title property of the Book class both in terms of name and type.

If you were to misspell the title parameter or use an incorrect type then a compilation error will occur.

You can alter the return type to return more results:

@Service(Book)
interface BookService {
    List<Book> findBooks(String title)
}

And if you wish to control pagination and query arguments you can add an args parameter that should be a Map:

@Service(Book)
interface BookService {
    List<Book> findBooks(String title, Map args)
}

In this case the following query will control pagination and ordering:

List<Book> books = bookService.findBooks(
    "The Stand",
    [offset:10, max:10, sort:'title', order:'desc']
)

You can include multiple parameters in the query:

@Service(Book)
interface BookService {
    List<Book> findBooks(String title, Date publishDate)
}

In this case a conjunction (AND) query will be executed. If you need to do a disjunction (OR) then it is time you learn about Dynamic Finder-style queries.

51.2.2. Dynamic Finder Queries

Dynamic finder styles queries use the stem plus the word By and then a Dynamic Finder expression.

For example:

@Service(Book)
interface BookService {
    List<Book> findByTitleAndPublishDateGreaterThan(String title, Date publishDate)
}

The signature above will produce a dynamic finder query using the method signature expression.

The possible method expressions are the same as those possible with GORM’s static Dynamic Finders

In this case the names of the properties to query are inferred from the method signature and the parameter names are not critical. If you misspell the method signature a compilation error will occur.

51.2.3. Where Queries

If you have a more complex query then you may want to consider using the @Where annotation:

    @Where({ title ==~ pattern && releaseDate > fromDate })
    Book searchBooks(String pattern, Date fromDate)

With the @Where annotation the method name can be anything you want and query is expressed within a closure passed with the @Where annotation.

The query will be type checked against the parameters and compilation will fail if you misspell a parameter or property name.

The syntax is the same as what is passed to GORM’s static where method, see the section on Where Queries for more information.

51.3. Query Joins

You can specify query joins using the @Join annotation:

import static jakarta.persistence.criteria.JoinType.*

@Service(Book)
interface BookService {
    @Join('author')
    Book find(String title) (1)

    @Join(value='author', type=LEFT) (2)
    Book findAnother(String title)
}
1 Join on the author property
2 Join on the author property using a LEFT OUTER join

51.3.1. JPA-QL Queries

If you need even more flexibility, then HQL queries can be used via the @Query annotation:

@Query("from $Book as book where book.title like $pattern")
Book searchByTitle(String pattern)

Note that in the example above, if you misspell the pattern parameter passed to the query a compilation error will occur.

However, if you were to incorrectly input the title property no error would occur since it is part of the String and not a variable.

You can resolve this by declaring the value of book within the passed GString:

@Query("from ${Book book} where ${book.title} like $pattern")
Book searchByTitle(String pattern)

In the above example if you misspell the title property then a compilation error will occur. This is extremely powerful as it gives you the ability to type check HQL queries, which has always been one of the disadvantages of using them in comparison to criteria.

This support for type checked queries extends to joins. For example consider this query:

@Query("""
 from ${Book book} (1)
 inner join ${Author author = book.author} (2)
 where $book.title = $title and $author.name = $author""") (3)
Book find(String title, String author)
1 Using from to define the root query
2 Use inner join and a declaration to define the association to join on
3 Apply any conditions in the where clause

51.3.2. Query Projections

There are a few ways to implement projections. One way is to is to use the convention T find[Domain Class][Property]. For example say the Book class has a releaseDate property of type Date:

@Service(Book)
interface BookService {
   Date findBookReleaseDate(String title)
}

This also works for multiple results:

@Service(Book)
interface BookService {
   List<Date> findBookReleaseDate(String publisher)
}

And you can use the Map argument to provide ordering and pagination if necessary:

@Service(Book)
interface BookService {
   List<Date> findBookReleaseDate(String publisher, Map args)
}
JPA-QL Projections

You can also use a JPA-QL query to perform a projection:

@Service(Book)
interface BookService {
   @Query("select $b.releaseDate from ${Book b} where $b.publisher = $publisher order by $b.releaseDate")
   List<Date> findBookReleaseDates(String publisher)
}
Interface Projections

Sometimes you want to expose a more limited set of a data to the calling class. In this case it is possible to use interface projections.

For example:

class Author {
    String name
    Date dateOfBirth (1)
}
interface AuthorInfo {
    String getName() (2)
}
@Service(Author)
interface AuthorService {
   AuthorInfo find(String name) (3)
}
1 The domain class Author has a property called dateOfBirth that we do not want to make available to the client
2 You can define an interface that only exposes the properties you want to expose
3 Return the interface from the service.
If a property exists on the interface but not on the domain class you will receive a compilation error.

51.4. Data Service Write Operations

Write operations in Data Services are automatically wrapped in a transaction. You can modify the transactional attributes by simply adding the @Transactional transformation to any method.

The following sections discuss the details of the different write operations.

51.4.1. Create

To create a new entity the method should return the new entity and feature either the parameters to be used to create the entity or the entity itself.

For example:

@Service(Book)
interface BookService {
    Book saveBook(String title)

    Book saveBook(Book newBook)
}

If any of the parameters don’t match up to a property on the domain class then a compilation error will occur.

If a validation error occurs then a ValidationException will be thrown from the service.

51.4.2. Update

Update operations are similar to Create operations, the main difference being that the first argument should be the id of the object to update.

For example:

@Service(Book)
interface BookService {
    Book updateBook(Serializable id, String title)
}

If any of the parameters don’t match up to a property on the domain class then a compilation error will occur.

If a validation error occurs then a ValidationException will be thrown from the service.

You can also implement update operations using JPA-QL:

@Query("update ${Book book} set ${book.title} = $newTitle where $book.title = $oldTitle")
Number updateTitle(String newTitle, String oldTitle)

51.4.3. Delete

Delete operations can either return void or return the instance that was deleted. In the latter case an extra query is required to fetch the entity prior to issue a delete.

@Service(Book)
interface BookService {
    Number deleteAll(String title)

    void delete(Serializable id)
}

You can also implement delete operations using JPA-QL:

@Query("delete ${Book book} where $book.title = $title")
void delete(String title)

Or via where queries:

@Where({ title == title && releaseDate > date })
void delete(String title, Date date)

51.5. Validating Data Services

GORM Data Services have built in support for jakarta.validation annotations for method parameters.

You will need to have a jakarta.validation implementation on your classpath (such as hibernate-validator and then simply annotate your method parameters using the appropriate annotation. For example:

import jakarta.validation.constraints.*

@Service(Book)
interface BookService {

    Book find(@NotNull String title)
}

In the above example the NotNull constraint is applied to the title property. If null is passed to the method a ConstraintViolationException exception will be thrown.

51.6. Data Services and Multiple Datasources

When using Data Services with multiple datasources, the service must declare which connection to use via the connection parameter of @Transactional.

51.6.1. Routing to a Secondary Datasource

Given a domain class mapped to a secondary datasource:

class Book {

    String title
    String author

    static mapping = {
        datasource 'books'
    }
}

Define an interface for your data access methods and an abstract class that declares the connection:

import grails.gorm.services.Service

interface BookDataService {

    Book get(Serializable id)

    Book save(Book book)

    void delete(Serializable id)

    List<Book> findAllByAuthor(String author)

    Long count()
}
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Service(Book)
@Transactional(connection = 'books')
abstract class BookService implements BookDataService {
    // All interface methods are auto-implemented by GORM
    // and route to the 'books' datasource automatically.
}

The @Transactional(connection = 'books') annotation on the abstract class ensures that all auto-implemented methods (get, save, delete, findBy*, countBy*, etc.) route to the books datasource. Without this annotation, queries silently route to the default datasource.

The @Service(Book) annotation identifies the domain class but does not determine which datasource to use. Even if Book declares datasource 'books' in its mapping block, you must specify @Transactional(connection = 'books') on the abstract class to route operations to the correct datasource.

51.6.2. How Connection Routing Works

When GORM compiles a @Service abstract class, the ServiceTransformation AST transform:

  1. Copies the @Transactional(connection = 'books') annotation from the abstract class to the generated implementation class

  2. For each auto-implemented method, resolves the connection identifier via findConnectionId()

  3. Generates method bodies that use the appropriate connection, routing CRUD operations and DetachedCriteria-based finder queries to the specified datasource

This means auto-implemented methods like get(), save(), delete(), findBy*(), countBy*(), @Where-annotated methods, @Query-annotated methods, and DetachedCriteria-based queries all respect the connection parameter without requiring manual implementations.

51.6.3. Complex Queries Using Domain Static Methods

Auto-implemented Data Service methods cover most query patterns, including dynamic finders with comparators, pagination, and property projections. For queries that require HQL, criteria builders, or aggregate functions, call domain static methods directly. When a domain class declares a non-default datasource in its mapping block, GORM registers its static API under that datasource. Methods like Book.executeQuery(), Book.createCriteria(), and Book.withCriteria() automatically route to the correct datasource:

import groovy.transform.CompileStatic
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional

@CompileStatic
@Service(Book)
@Transactional(connection = 'books')
abstract class BookService implements BookDataService {

    // Auto-implemented methods from interface are inherited
    // and route to 'books' datasource automatically.

    List getTopAuthors(int limit) {
        Book.executeQuery('''
            SELECT b.author, COUNT(b) as bookCount
            FROM Book b
            GROUP BY b.author
            ORDER BY bookCount DESC
        ''', Collections.emptyMap(), [max: limit])
    }

    List<Book> searchWithCriteria(String titlePattern, String author) {
        Book.createCriteria().list {
            like('title', "%${titlePattern}%")
            eq('author', author)
            order('title', 'asc')
        } as List<Book>
    }
}

These domain static methods all route to the datasource declared in the domain’s mapping block:

Method Description

Book.executeQuery(String hql, Map params)

HQL/JPQL queries

Book.executeUpdate(String hql, Map params)

Bulk UPDATE/DELETE statements

Book.withCriteria(Closure criteria)

Criteria query

Book.createCriteria()

Criteria builder for complex queries

Book.where(Closure query)

Where query

Book.withTransaction(Closure action)

Manual transaction management

Book.withNewSession(Closure action)

Obtain a new session for the domain’s datasource

Book.count()

Total record count

Book.get(Serializable id)

Find by primary key

Book.list(Map params)

Paginated list

Domain static methods work under @CompileStatic and route to the correct datasource automatically. The namespace syntax (e.g., Book.books.get(42)) is only needed when a domain is mapped to multiple datasources and you need to target a specific one.

51.6.4. Consuming Multi-Datasource Data Services

Other services inject the Data Service interface type. Spring resolves the abstract class bean automatically:

import groovy.transform.CompileStatic

@CompileStatic
class LibraryService {

    BookDataService bookDataService  // injected automatically

    Map getLibraryStats() {
        Long totalBooks = bookDataService.count()
        List<Book> recentBooks = bookDataService.findAllByAuthor('Tolkien')
        [total: totalBooks, tolkienBooks: recentBooks.size()]
    }
}

The consuming service does not need @Transactional(connection = 'books'). The Data Service handles datasource routing internally.

Data Services can also inject other Data Services. @CompileStatic works on @Service abstract classes that declare @Service-typed properties:

import grails.gorm.services.Service

interface AuthorDataService {

    Author get(Serializable id)

    Author save(Author author)
}
import groovy.transform.CompileStatic
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional

@CompileStatic
@Service(Author)
@Transactional(connection = 'books')
abstract class AuthorService implements AuthorDataService {

    BookDataService bookDataService  // injected @Service property

    Map getAuthorWithBooks(Serializable authorId) {
        Author author = get(authorId)
        List<Book> books = bookDataService.findAllByAuthor(author.name)
        [author: author, books: books]
    }
}

When the Spring context initializes the generated implementation class, it autowires all @Service-typed properties by type. By the time any user code runs, injected Data Services are fully available.

51.6.5. Multi-Tenancy with Multiple Datasources

Domain classes that use the MultiTenant trait and declare an explicit non-default datasource (e.g., datasource 'analytics') route correctly through Data Services. GORM preserves explicit datasource qualifiers for multi-tenant entities, so Data Services using @Transactional(connection = 'analytics') work the same way as for non-tenant domain classes:

import grails.gorm.MultiTenant

class Metric implements MultiTenant<Metric> {

    String name
    BigDecimal value

    static mapping = {
        datasource 'analytics'
    }
}
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Service(Metric)
@Transactional(connection = 'analytics')
abstract class MetricService {

    abstract Metric get(Serializable id)

    abstract Metric save(Metric metric)

    abstract List<Metric> list()
}

The connection parameter on the abstract class routes all auto-implemented operations - including save(), get(), and delete() - to the analytics datasource, regardless of multi-tenancy mode (DATABASE or DISCRIMINATOR).

51.7. RxJava Support

GORM Data Services also support returning RxJava 1.x rx.Observable or rx.Single types.

RxJava 2.x support is planned for a future release

To use the RxJava support you need to ensure that the grails-datastore-gorm-rx dependencies is on the classpath by adding the following to build.gradle:

build.gradle
compile "org.grails:grails-datastore-gorm-rx:8.0.0"

For example:

import rx.*

@Service(Book)
interface BookService {
   Single<Book> findOne(String title)
}

When a rx.Single is used then a single result is returned. To query multiple results use an rx.Observable instead:

import rx.*

@Service(Book)
interface BookService {
   Observable<Book> findBooks(String title)
}

For regular GORM entities, GORM will by default execute the persistence operation using RxJava’s IO Scheduler.

For RxGORM entities where the underlying database supports non-blocking access the database driver will schedule the operation accordingly.

You can run the operation on a different scheduler using the RxSchedule annotation:

import rx.*
import grails.gorm.rx.services.RxSchedule
import grails.gorm.services.Service
import rx.schedulers.Schedulers

@Service(Book)
interface BookService {

   @RxSchedule(scheduler = { Schedulers.newThread() })
   Observable<Book> findBooks(String title)
}

52. Multiple Data Sources

GORM supports the notion of multiple data sources where multiple individual SQL DataSource instances can be configured and switched between.

52.1. Configuring Multiple Data Sources

To configure multiple data sources you need to use the dataSources setting. For example in application.yml:

dataSource:
    pooled: true
    dbCreate: create-drop
    url: jdbc:h2:mem:books
    driverClassName: org.h2.Driver
    username: sa
    password:
dataSources:
    moreBooks:
        url: jdbc:h2:mem:moreBooks
        hibernate:
            readOnly: true
    evenMoreBooks:
        url: jdbc:h2:mem:evenMoreBooks

You can configure individual settings for each data source. If a setting is not specified by default the setting is inherited from the default data source, so in the example above there is no need to specify the driverClassName for each data source if the same driver is used for all.

For more information on configuration see the Configuration section.

52.2. Mapping Domain Classes to Data Sources

If a domain class has no DataSource configuration, it defaults to the standard 'dataSource'. Set the datasource property in the mapping block to configure a non-default DataSource. For example, if you want to use the ZipCode domain to use a DataSource called 'lookup', configure it like this:

class ZipCode {

   String code

   static mapping = {
      datasource 'lookup'
   }
}

A domain class can also use two or more configured DataSource instances. Use the datasources property with a list of names to configure more than one, for example:

class ZipCode {

   String code

   static mapping = {
      datasources(['lookup', 'auditing'])
   }
}

If a domain class uses the default DataSource and one or more others, you can use the ConnectionSource.DEFAULT constant to indicate that:

import org.grails.datastore.mapping.core.connections.*

class ZipCode {

   String code

   static mapping = {
      datasources(['lookup', ConnectionSource.DEFAULT])
   }
}

If a domain class uses all configured DataSource instances use the value ALL:

import org.grails.datastore.mapping.core.connections.*

class ZipCode {

   String code

   static mapping = {
      datasource ConnectionSource.ALL
   }
}

52.3. Data Source Namespaces

If a domain class uses more than one DataSource then you can use the namespace implied by each DataSource name to make GORM calls for a particular DataSource. For example, consider this class which uses two DataSource instances:

class ZipCode {

   String code

   static mapping = {
      datasources(['lookup', 'auditing'])
   }
}

The first DataSource specified is the default when not using an explicit namespace, so in this case we default to lookup. But you can call GORM methods on the auditing DataSource with the DataSource name, for example:

def zipCode = ZipCode.auditing.get(42)
...
zipCode.auditing.save()

As you can see, you add the DataSource to the method call in both the static case and the instance case.

A session or transaction opened through a namespace covers the whole block. Inside ZipCode.auditing.withTransaction { } or ZipCode.auditing.withNewSession { }, the calls on ZipCode itself use the auditing DataSource, whose session and transaction are the ones open:

ZipCode.auditing.withTransaction {
    def zipCode = ZipCode.get(42)    // read from 'auditing'
    zipCode.code = '99501'
    zipCode.save()                   // written to 'auditing', in its transaction
}

Other domain classes are not affected, and an operation inside the block that names a DataSource, such as ZipCode.lookup.count(), uses the one it names. A method annotated @Transactional(connection = 'auditing') routes in the same way, for every domain class mapped to the auditing DataSource; a class that is not mapped to it keeps its own, and a multi-tenant class is left to its tenant, since a transaction opened for a DataSource does not say which tenant the operations in it belong to.

You can use Where queries:

def results = ZipCode.where {
    code ==~ '995%'
}.withConnection('auditing').list()

or Criteria queries:

def c = ZipCode.auditing.createCriteria()
def results = c.list {
    like('code','995%')
}
The namespace syntax (e.g., ZipCode.auditing.get(42)) is only needed when a domain is mapped to multiple datasources and you need to target a specific one. For domains mapped to a single non-default datasource, static methods like ZipCode.get(42) route to the correct datasource automatically and work under @CompileStatic. You can also use a Data Service with @Transactional(connection = 'auditing') for automatic routing of auto-implemented methods. See the Data Services and Multiple Datasources section for the full pattern.

52.4. Using Data Services with Multiple Datasources

GORM Data Services support multi-datasource routing via @Transactional(connection = 'connectionName') on the abstract class. All auto-implemented methods - get(), save(), delete(), findBy*(), countBy*() - route to the specified datasource automatically. For complex queries, domain static methods like Book.executeQuery() and Book.createCriteria() also route to the correct datasource. This works with @CompileStatic, injected @Service properties, and MultiTenant domain classes. See the Data Services and Multiple Datasources section for the full pattern.

52.5. The ConnectionSources API

Introduced in GORM 6.0, the ConnectionSources API allows you to introspect the data sources configured for the application:

@Autowired
HibernateDatastore hibernateDatastore
...
ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> connectionSources
                                        = hibernateDatastore.getConnectionSources()

for(ConnectionSource<SessionFactory, HibernateConnectionSourceSettings> connectionSource in connectionSources) {
        println "Name $connectionSource.name"
        SessionFactory sessionFactory = connectionSource.source
}

53. Multi-Tenancy

Multi-Tenancy, as it relates to software developments, is when a single instance of an application is used to service multiple clients (tenants) in a way that each tenants' data is isolated from the other.

This type of architecture is highly common in Software as a Service (SaaS) and Cloud architectures. There are a variety of approaches to multi-tenancy and GORM tries to be flexible in supporting as many as possible.

53.1. Multi-Tenancy Modes

GORM supports the following different multitenancy modes:

  • DATABASE - A separate database with a separate connection pool is used to store each tenants data.

  • SCHEMA - The same database, but different schemas are used to store each tenants data.

  • DISCRIMINATOR - The same database is used with a discriminator used to partition and isolate data.

The above modes are listed from least probable to leak data between tenants to most probable. When using a DISCRIMINATOR approach much greater care needs to be taken to ensure tenants don’t see each other’s data.

53.2. Multi-Tenancy Transformations

The following transformations can be applied to any class to simplify greatly the development of Multi-Tenant applications. These include:

  • @CurrentTenant - Resolve the current tenant for the context of a class or method

  • @Tenant - Use a specific tenant for the context of a class or method

  • @WithoutTenant - Execute logic without a specific tenant (using the default connection)

For example:

import grails.gorm.multitenancy.*

// resolve the current tenant for every method
@CurrentTenant
class TeamService {

    // execute the countPlayers method without a tenant id
    @WithoutTenant
    int countPlayers() {
        Player.count()
    }

    // use the tenant id "another" for all GORM logic within the method
    @Tenant({"another"})
    List<Team> allTwoTeams() {
        Team.list()
    }

    List<Team> listTeams() {
        Team.list(max:10)
    }

    @Transactional
    void addTeam(String name) {
        new Team(name:name).save(flush:true)
    }
}

53.3. Database Per Tenant

Using a database per tenant is the most secure way to isolate each tenants data and builds upon GORM’s existing support for Multiple Data Sources.

53.3.1. Configuration

In order to activate database-per-tenant multi-tenancy you need to set the multi-tenancy mode to DATABASE in your configuration and supply a TenantResolver:

grails:
    gorm:
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SubDomainTenantResolver
dataSource:
    dbCreate: create-drop
    url: jdbc:h2:mem:books
dataSources:
    moreBooks:
        url: jdbc:h2:mem:moreBooks
    evenMoreBooks:
        url: jdbc:h2:mem:evenMoreBooks

The above example uses a built-in TenantResolver implementation that works with Grails or Spring Boot and evaluates the current tenant id from the DNS sub-domain in a web application. However, you can implement whatever tenant resolving strategy you choose.

53.3.2. Multi Tenant Domain Classes

With the above configuration in place you then to need to implement the MultiTenant trait in the domain classes you want to be regarded as multi tenant:

class Book implements MultiTenant<Book> {
    String title
}

With that done whenever you attempt to execute a method on a domain class the tenant id will be resolved via the TenantResolver. So for example if the TenantResolver returns moreBooks then the moreBooks connection will be used when calling GORM methods such as save(), list() and so on.

53.3.3. Multi Tenancy and the Session Factory

Note that if you reference the default SessionFactory or PlatformTransactionManager in your classes that are injected via Spring, these will not be tenant aware and will point directly to default data source.

If you wish to obtain a specific SessionFactory or PlatformTransactionManager then you can use the getDatastoreForConnection(name) method of the HibernateDatastore class:

@Autowired
HibernateDatastore hibernateDatastore
...
Serializable tenantId = Tenants.currentId(HibernateDatastore)
SessionFactory sessionFactory = hibernateDatastore
                                    .getDatastoreForConnection(tenantId.toString())
                                    .getSessionFactory()

53.3.4. Multi Tenancy with Sessions

When working with GORM typically you need a session bound to the current thread in order for GORM and transactions to work consistently. If you switch to a different tenant then it may be that the session bound to the current thread no longer matches the underlying SessionFactory being used by the tenant. With this in mind you may wants to use the Tenants class to ensure the correct session is bound for the current tenant:

import static grails.gorm.multitenancy.Tenants.*

List<Book> books = withCurrent {
    Book.list()
}

You can also use a specify tenant id using the withId method:

import static grails.gorm.multitenancy.Tenants.*

List<Book> books = withId("moreBooks") {
    Book.list()
}

Note that if you are using more than one GORM implementation, it may be necessary to specify the implementation type:

import static grails.gorm.multitenancy.Tenants.*
import org.grails.orm.hibernate.*

List<Book> books = withId(HibernateDatastore, "moreBooks") {
    Book.list()
}

53.3.5. Adding Tenants at Runtime

Provisioning and creating SQL databases at runtime is non-trivial and beyond the scope of what GORM offers, however the ConnectionSources API does provide a way to hook into GORM to make this possible.

By default an InMemoryConnectionSources object is used which will read the connection sources from the application configuration and store them in-memory.

However, it is possible to configure an alternate implementation:

grails:
    gorm:
        connectionSourcesClass: com.example.MyConnectionSources
        multiTenancy:
            mode: DATABASE

The implementation could read the connection sources at startup from another database table and implement logic by overriding the addConnectionSource method to provision a new databases at runtime.

If you are interested in more examples, an implementation exists for MongoDB that reads connection sources from a MongoDB collection. However, it doesn’t implement support for provision MongoDB instances at runtime.

53.4. Schema Per Tenant

Schema-per-tenant is when a single database is used, but a different database schema is used for each tenant.

53.4.1. Configuration

In order to activate schema-per-tenant multi-tenancy you need to set the multi-tenancy mode to SCHEMA in your configuration and supply a TenantResolver:

grails:
    gorm:
        multiTenancy:
            mode: SCHEMA
            tenantResolverClass: foo.bar.MySchemaResolver
dataSource:
    dbCreate: create-drop
    url: jdbc:h2:mem:books

The TenantResolver can optionally implement the AllTenantsResolver interface and return the schema names of all tenants. This gives you the option to hard code these, place them in configuration or read them from the default schema dynamically.

If the AllTenantsResolver resolver is not implemented then GORM will use the configured SchemaHandler to resolve all the schema names from the database which it will use for the tenants.

53.4.2. Runtime Schema Creation

On startup, if dataSource.dbCreate is set to create the database at runtime, then GORM will attempt to create the schemas if they are missing.

To do this it uses an instance SchemaHandler which by default uses the CREATE SCHEMA [name] syntax, but can be overridden and customized for other database dialects as necessary.

If you want to use this feature and create schemas at runtime depending on the database you may need to configure an alternate implementation:

dataSource:
    dbCreate: create-drop
    schemaHandler: foo.bar.MySchemaHandler

You can disable completely runtime schema creation by removing the dbCreate option or setting it to none.

If you wish to add a schema whilst the application is running then you can use the addTenantForSchema of the HibernateDatastore class:

HibernateDatastore datastore = ...
datastore.addTenantForSchema("myNewSchema")
If dbCreate is disabled then you will have to create the schema manually prior to invoking this method

53.4.3. Schema-Per-Tenant Caveats

In order to support a schema-per-tenant, just like the DATABASE Multi-Tenancy mode, GORM uses a unique SessionFactory per tenant. So all of the same considerations regarding session management apply.

53.5. Partitioned Multi-Tenancy

Partitioned multi-tenancy is when a discriminator column is used in each multi-tenant class in order to partition the data.

When using a discriminator all data for all tenants is stored in a single database. This is means that, as a developer, you have to take much greater care to ensure that each tenants' data is correctly isolated.

53.5.1. Configuration

In order to activate discriminator-based multi-tenancy you need to set the multi-tenancy mode to DISCRIMINATOR in your configuration and supply a TenantResolver:

grails:
    gorm:
        multiTenancy:
            mode: DISCRIMINATOR
            tenantResolverClass: foo.bar.MyTenantResolver
dataSource:
    dbCreate: create-drop
    url: jdbc:h2:mem:books
The specified TenantResolver will also need to implement the AllTenantsResolver interface, which has an additional method to resolve all known tenants.

53.5.2. Mapping Domain Classes

Like the other forms of multi-tenancy you then need to implement the MultiTenant trait in the domain classes that are multi-tenant. In addition, you also need to specify a discriminator column. The default discriminator column is called tenantId, for example:

class Book implements MultiTenant<Book> {
    Long tenantId
    String title
}

However, the tenant identifier can be any type or name. For example:

class Book implements MultiTenant<Book> {
    String publisher
    String title

    static mapping = {
        tenantId name:'publisher'
    }
}

53.5.3. Querying and Caveats

The discriminator-based multi-tenancy transparently adds a Hibernate filter that is activated when you query the domain class and you can also use the Tenants API to switch between tenants:

import static grails.gorm.multitenancy.Tenants.*

List<Book> books = withCurrent {
    Book.list()
}
...
List<Book> books = withId("moreBooks") {
    Book.list()
}

Note that this automatic activation of the Hibernate filter using GORM eventing and therefore only works when you use GORM methods, if you perform any raw Hibernate queries on the session factory you will need to activate the filter manually:

import static grails.gorm.multitenancy.Tenants.*
import org.grails.orm.hibernate.*

// get a reference to the datastore and sessionFactory, probably from Spring
HibernateDatastore datastore = ..
SessionFactory sessionFactory = ..
datastore.enableMultiTenancyFilter()

// do work with the session factory
sessionFactory.openSession()

53.6. Understanding Tenant Resolvers

As mentioned previously the TenantResolver interface is how you define the value of the current tenant.

This section will cover a few more details about implementing TenantResolver instances.

53.6.1. Specifying the Tenant Resolver

As already mentioned you can specify the TenantResolver via configuration using the grails.gorm.tenantResolverClass setting:

grails:
    gorm:
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SubDomainTenantResolver

However, if you are using Grails or Spring Boot then the TenantResolver can also be specified as a Spring bean and it will automatically be injected, this allows you to use dependency injection to configure the dependencies of the resolver.

53.6.2. Built-in Tenant Resolvers

The following table contains all of the TenantResolver implementations that ship with GORM and are usable out of the box. The package name has been shorted from org.grails.datastore.mapping to o.g.d.m for brevity:

name description

o.g.d.m.multitenancy.resolvers.FixedTenantResolver

Resolves against a fixed tenant id

o.g.d.m.multitenancy.resolvers.SystemPropertyTenantResolver

Resolves the tenant id from a system property called gorm.tenantId

o.g.d.m.multitenancy.web.SubDomainTenantResolver

Resolves the tenant id from the subdomain via DNS

o.g.d.m.multitenancy.web.CookieTenantResolver

Resolves the current tenant from an HTTP cookie named gorm.tenantId by default

o.g.d.m.multitenancy.web.SessionTenantResolver

Resolves the current tenant from the HTTP session using the attribute gorm.tenantId by default

o.g.d.m.multitenancy.web.HttpHeaderTenantResolver

Resolves the current tenant from the request HTTP Header using the header name gorm.tenantId by default

The tenant resolvers in the org.grails.datastore.mapping.multitenancy.web package require the grails-datastore-web dependency:

build.gradle
compile "org.grails:grails-datastore-web:8.0.0"

53.6.3. The AllTenantsResolver interface

If you are using discriminator-based multi-tenancy then you may need to implement the AllTenantsResolver interface in your TenantResolver implementation if you want to at any point iterate over all available tenants.

Typically with discriminator-based multi-tenancy the tenants are identified by some other domain class property. So for example an implementation would look like:

Iterable<Serializable> resolveTenantIds() {
    new DetachedCriteria(Company)
            .distinct('name')
            .list()
}

The above example uses the distinct names of each Company domain class to resolve all of the tenant identifiers.

53.6.4. Implementing Web Tenant Resolvers

If you wish to implement your own tenant resolver for Grails or Spring Boot then it is possible do so using the RequestContextHolder class without needing to inject any dependencies. For example the SubDomainTenantResolver implementation is as follows:

Serializable resolveTenantIdentifier() {

    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes()
    if(requestAttributes instanceof ServletWebRequest) {

        String subdomain = ((ServletWebRequest)requestAttributes).getRequest().getRequestURL().toString();
        subdomain = subdomain.substring(subdomain.indexOf("/") + 2);
        if( subdomain.indexOf(".") > -1 ) {
            return subdomain.substring(0, subdomain.indexOf("."))
        }
        else {
            return ConnectionSource.DEFAULT
        }
    }
    throw new TenantNotFoundException("Tenant could not be resolved outside a web request")
}
If the tenant id is not found a TenantNotFoundException should be thrown.

54. Validation and Constraints

Constraints are how you define validation when using GORM entities.

54.1. Applying Constraints

Within a domain class constraints are defined with the constraints property that is assigned a code block:

class User {
    String login
    String password
    String email
    Integer age

    static constraints = {
      ...
    }
}

You then use method calls that match the property name for which the constraint applies in combination with named parameters to specify constraints:

class User {
    ...

    static constraints = {
        login size: 5..15, blank: false, unique: true
        password size: 5..15, blank: false
        email email: true, blank: false
        age min: 18
    }
}

In this example we’ve declared that the login property must be between 5 and 15 characters long, it cannot be blank and must be unique. We’ve also applied other constraints to the password, email and age properties.

By default, all domain class properties are not nullable (i.e. they have an implicit nullable: false constraint).

Note that constraints are only evaluated once which may be relevant for a constraint that relies on a value like an instance of java.util.Date.

class User {
    ...

    static constraints = {
        // this Date object is created when the constraints are evaluated, not
        // each time an instance of the User class is validated.
        birthDate max: new Date()
    }
}

54.2. Referencing Instances in Constraints

It’s very easy to attempt to reference instance variables from the static constraints block, but this isn’t legal in Groovy (or Java). If you do so, you will get a MissingPropertyException for your trouble. For example, you may try the following:

class Response {
    Survey survey
    Answer answer

    static constraints = {
        survey blank: false
        answer blank: false, inList: survey.answers
    }
}

See how the inList constraint references the instance property survey? That won’t work. Instead, use a custom validator constraint:

class Response {
    ...
    static constraints = {
        survey blank: false
        answer blank: false, validator: { val, Response obj -> val in obj.survey.answers }
    }
}

In this example, the obj argument to the custom validator is the domain instance that is being validated, so we can access its survey property and return a boolean to indicate whether the new value for the answer property, val, is valid.

54.3. Cascade constraints validation

If GORM entity references some other entities, then during its constraints evaluation (validation) the constraints of the referenced entity could be evaluated also, if needed. There is a special parameter cascadeValidate in the entity mappings section, which manage the way of this cascaded validation happens.

class Author {
    Publisher publisher

    static mapping = {
        publisher(cascadeValidate: "dirty")
    }
}

class Publisher {
    String name

    static constraints = {
        name blank: false
    }
}

The following table presents all options, which can be used:

Option Description

none

Will not do any cascade validation at all for the association.

default

The DEFAULT option. GORM performs cascade validation in some cases.

dirty

Only cascade validation if the referenced object is dirty via the DirtyCheckable trait. If the object doesn’t implement DirtyCheckable, this will fall back to default.

owned

Only cascade validation if the entity owns the referenced object.

It is possible to set the global option for the cascadeValidate:

grails-app/conf/application.groovy
grails.gorm.default.mapping = {
    '*'(cascadeValidate: 'dirty')
}

54.4. Constraints Reference

The following table summarizes the available constraints with a brief example:

Constraint Description Example

blank

Validates that a String value is not blank

login(blank:false)

creditCard

Validates that a String value is a valid credit card number

cardNumber(creditCard: true)

email

Validates that a String value is a valid email address.

homeEmail(email: true)

inList

Validates that a value is within a range or collection of constrained values.

name(inList: ["Joe"])

matches

Validates that a String value matches a given regular expression.

login(matches: "[a-zA-Z]+")

max

Validates that a value does not exceed the given maximum value.

age(max: new Date()) price(max: 999F)

maxSize

Validates that a value’s size does not exceed the given maximum value.

children(maxSize: 25)

min

Validates that a value does not fall below the given minimum value.

age(min: new Date()) price(min: 0F)

minSize

Validates that a value’s size does not fall below the given minimum value.

children(minSize: 25)

notEqual

Validates that that a property is not equal to the specified value

login(notEqual: "Bob")

nullable

Allows a property to be set to null - defaults to false.

age(nullable: true)

range

Uses a Groovy range to ensure that a property’s value occurs within a specified range

age(range: 18..65)

scale

Set to the desired scale for floating point numbers (i.e. the number of digits to the right of the decimal point).

salary(scale: 2)

size

Uses a Groovy range to restrict the size of a collection or number or the length of a String.

children(size: 5..15)

unique

Constrains a property as unique at the database level

login(unique: true)

url

Validates that a String value is a valid URL.

homePage(url: true)

validator

Adds custom validation to a field.

See documentation

54.5. Constraints and Database Mapping

Although constraints are primarily for validation, it is important to understand that constraints can affect the way in which the database schema is generated.

Where feasible, GORM uses a domain class’s constraints to influence the database columns generated for the corresponding domain class properties.

Consider the following example. Suppose we have a domain model with the following properties:

String name
String description

By default, in MySQL, GORM would define these columns as

Column Data Type

name

varchar(255)

description

varchar(255)

But perhaps the business rules for this domain class state that a description can be up to 1000 characters in length. If that were the case, we would likely define the column as follows if we were creating the table with an SQL script.

Column Data Type

description

TEXT

Chances are we would also want to have some application-based validation to make sure we don’t exceed that 1000 character limit before we persist any records. In GORM, we achieve this validation with constraints. We would add the following constraint declaration to the domain class.

static constraints = {
    description maxSize: 1000
}

This constraint would provide both the application-based validation we want and it would also cause the schema to be generated as shown above. Below is a description of the other constraints that influence schema generation.

54.5.1. Constraints Affecting String Properties

  • inList

  • maxSize

  • size

If either the maxSize or the size constraint is defined, Grails sets the maximum column length based on the constraint value.

In general, it’s not advisable to use both constraints on the same domain class property. However, if both the maxSize constraint and the size constraint are defined, then GORM sets the column length to the minimum of the maxSize constraint and the upper bound of the size constraint. (GORM uses the minimum of the two, because any length that exceeds that minimum will result in a validation error.)

If the inList constraint is defined (and the maxSize and the size constraints are not defined), then GORM sets the maximum column length based on the length of the longest string in the list of valid values. For example, given a list including values "Java", "Groovy", and "C++", GORM would set the column length to 6 (i.e., the number of characters in the string "Groovy").

54.5.2. Constraints Affecting Numeric Properties

  • min

  • max

  • range

If the max, min, or range constraint is defined, GORM attempts to set the column precision based on the constraint value. (The success of this attempted influence is largely dependent on how Hibernate interacts with the underlying DBMS.)

In general, it’s not advisable to combine the pair min/max and range constraints together on the same domain class property. However, if both of these constraints is defined, then GORM uses the minimum precision value from the constraints. (GORM uses the minimum of the two, because any length that exceeds that minimum precision will result in a validation error.)

  • scale

If the scale constraint is defined, then GORM attempts to set the column scale based on the constraint value. This rule only applies to floating point numbers (i.e., java.lang.Float, java.Lang.Double, java.lang.BigDecimal, or subclasses of java.lang.BigDecimal). The success of this attempted influence is largely dependent on how Hibernate interacts with the underlying DBMS.

The constraints define the minimum/maximum numeric values, and GORM derives the maximum number of digits for use in the precision. Keep in mind that specifying only one of min/max constraints will not affect schema generation (since there could be large negative value of property with max:100, for example), unless the specified constraint value requires more digits than default Hibernate column precision is (19 at the moment). For example:

someFloatValue max: 1000000, scale: 3

would yield:

someFloatValue DECIMAL(19, 3) // precision is default

but

someFloatValue max: 12345678901234567890, scale: 5

would yield:

someFloatValue DECIMAL(25, 5) // precision = digits in max + scale

and

someFloatValue max: 100, min: -100000

would yield:

someFloatValue DECIMAL(8, 2) // precision = digits in min + default scale

55. GORM and Testing

In previous versions of GORM it was much more difficult to setup a unit test to test your GORM logic.

However, since GORM 6.0, this situation has changed and it is relatively trivial to setup GORM for testing.

55.1. Unit Testing with Spock

Spock is the recommended tool for writing unit tests with GORM and is trivial to setup.

55.1.1. GORM with Hibernate and Spock Basics

The following is an example Spock unit test:

import spock.lang.*
import grails.gorm.annotation.Entity
import org.grails.orm.hibernate.HibernateDatastore

class ExampleSpec extends Specification { (1)

    @Shared @AutoCleanup HibernateDatastore hibernateDatastore (2)

    void setupSpec() {
       hibernateDatastore = new HibernateDatastore(Person) (3)
    }

    void "test something"() { (4)
       // your logic here
    }
}

@Entity (5)
class Person {
    ...
}
1 The test should extend spock.lang.Specification
2 The Shared annotation is used to indicate to Spock that the HibernateDatastore is shared across all tests. The AutoCleanup annotation makes sure that HibernateDatastore is shutdown when all tests finish executing.
3 Within the setupSpec method a new HibernateDatastore is constructed with the classes to use as the argument to the constructor.
4 You then write your test logic within each method
5 You can inline domain classes within the unit test if you annotate them with @Entity

55.1.2. Spock and Transactions

Note that in general you have to wrap your test execution logic in a session or transaction. The easiest way to do this is with grails.gorm.transactions.Transactional:

...
import grails.gorm.transactions.*
import org.springframework.transaction.PlatformTransactionManager

class ExampleSpec extends Specification {

    @Shared @AutoCleanup HibernateDatastore hibernateDatastore
    @Shared PlatformTransactionManager transactionManager (1)

    void setupSpec() {
        hibernateDatastore = new HibernateDatastore(Person)
        transactionManager = hibernateDatastore.getTransactionManager() (2)
    }

    @Transactional (3)
    def setup() {
        new Person(firstName:"Fred").save()
    }

    @Rollback (4)
    void "test execute GORM standalone in a unit test"() {
        // your logic here
    }
}
1 The PlatformTransactionManager is defined as a Shared field
2 You can obtain the PlatformTransactionManager from the HibernateDatastore
3 The Transactional annotation is used to setup test data
4 The Rollback annotation is used to rollback any changes made within each test

In the example above, each test method is wrapped in a transaction that rolls back any changes using the grails.gorm.transactions.Rollback annotation.

If you want to setup some test data within the setupSpec method that is shared across all tests then you can use withTransaction:
...
void setupSpec() {
    hibernateDatastore = new HibernateDatastore(Person)
    ...
    Person.withTransaction {
        new Person(firstName:"Fred").save()
    }
}

55.1.3. Configuring GORM in Spock

If you need to configure GORM within a Spock unit test you can pass a map to the constructor of HibernateDatastore. For example to setup multi-tenancy:

...
void setupSpec() {
    Map configuration = [
        'grails.gorm.multiTenancy.mode':'DISCRIMINATOR',
        'grails.gorm.multiTenancy.tenantResolverClass':SystemPropertyTenantResolver
    ]
    hibernateDatastore = new HibernateDatastore(configuration, Person)
    ...
}

55.2. Unit Testing with JUnit

To unit test with JUnit it is largely similar to Spock, just following different idioms.

So instead of setupSpec use @BeforeClass:

import org.junit.*
import grails.gorm.transactions.*
import org.grails.orm.hibernate.HibernateDatastore
import org.springframework.transaction.PlatformTransactionManager

class ExampleTest  {

    static HibernateDatastore hibernateDatastore

    PlatformTransactionManager transactionManager

    @BeforeClass
    void setupGorm() {
       hibernateDatastore = new HibernateDatastore(Person)
    }

    @AfterClass
    void shutdownGorm() {
       hibernateDatastore.close()
    }

    @Before
    void setup() {
        transactionManager = hibernateDatastore.getTransactionManager()
    }

    @Rollback
    @Test
    void testSomething() {
       // your logic here
    }
}
JUnit doesn’t have anything like Spock’s AutoCleanup so you must call close() on the HibernateDatastore manually!

56. Database Migration Plugin

56.1. Introduction

The Database Migration plugin helps you manage database changes while developing Grails applications. The plugin uses the Liquibase library.

Using this plugin (and Liquibase in general) adds some structure and process to managing database changes. It will help avoid inconsistencies, communication issues, and other problems with ad-hoc approaches.

Database migrations are represented in text form, either using a Groovy DSL or native Liquibase XML, in one or more changelog files. This approach makes it natural to maintain the changelog files in source control and also works well with branches. Changelog files can include other changelog files, so often developers create hierarchical files organized with various schemes. One popular approach is to have a root changelog named changelog.groovy (or changelog.xml) and to include a changelog per feature/branch that includes multiple smaller changelogs. Once the feature is finished and merged into the main development tree/trunk the changelog files can either stay as they are or be merged into one large file. Use whatever approach makes sense for your applications, but keep in mind that there are many options available for changelog management.

Individual changes have an ID that should be globally unique, although they also include the username of the user making the change, making the combination of ID and username unique (although technically the ID, username, and changelog location are the "unique key").

As you make changes in your code (typically domain classes) that require changes in the database, you add a new change set to the changelog. Commit the code changes along with the changelog additions, and the other developers on your team will get both when they update from source control. Once they apply the new changes their code and development database will be in sync with your changes. Likewise when you deploy to a QA, a staging server, or production, you’ll run the un-run changes that correspond to the code updates to being that environment’s database in sync. Liquibase keeps track of previously executed changes so there’s no need to think about what has and hasn’t been run yet.

Scripts

Your primary interaction with the plugin will be using the provided scripts. For the most part these correspond to the many Liquibase commands that are typically executed directly from the commandline or with its Ant targets, but there are also a few Grails-specific scripts that take advantage of the information available from the GORM mappings.

All the scripts start with dbm- to ensure that they’re unique and don’t clash with scripts from Grails or other plugins.

56.2. Getting Started

The first step is to add a dependency for the plugin in build.gradle:

buildscript {
   dependencies {
      ...
      classpath 'org.apache.grails:grails-data-hibernate7-dbmigration:8.0.0'
   }
}

dependencies {
   ...
     implementation 'org.apache.grails:grails-data-hibernate7-dbmigration:8.0.0'
}

Typical initial workflow

Next you’ll need to create an initial changelog. You can use Liquibase XML or the plugin’s Groovy DSL for individual files. You can even mix and match; Groovy files can include other Groovy files and Liquibase XML files (but XML files can’t include Groovy files).

Depending on the state of your database and code, you have two options; either create a changelog from the database or create it from your domain classes. The decision tends to be based on whether you prefer to design the database and adjust the domain classes to work with it, or to design your domain classes and use Hibernate to create the corresponding database structure.

To create a changelog from the database, use the dbm-generate-changelog script:

grails dbm-generate-changelog changelog.groovy

or

grails dbm-generate-changelog changelog.xml

depending on whether you prefer the Groovy DSL or XML. The filename is relative to the changelog base folder, which defaults to grails-app/migrations.

If you use the XML format (or use a non-default Groovy filename), be sure to change the name of the file in application.groovy so dbm-update and other scripts find the file:
grails.plugin.databasemigration.changelogFileName = 'changelog.xml'

Since the database is already correct, run the dbm-changelog-sync script to record that the changes have already been applied:

grails dbm-changelog-sync

Running this script is primarily a no-op except that it records the execution(s) in the Liquibase DATABASECHANGELOG table.

To create a changelog from your domain classes, use the dbm-generate-gorm-changelog script:

grails dbm-generate-gorm-changelog changelog.groovy

or

grails dbm-generate-gorm-changelog changelog.xml

If you haven’t created the database yet, run the dbm-update script to create the corresponding tables:

grails dbm-update

or the dbm-changelog-sync script if the database is already in sync with your code:

grails dbm-changelog-sync

Source control

Now you can commit the changelog and the corresponding application code to source control. Other developers can then update and synchronize their databases, and start doing migrations themselves.

56.3. Configuration

There are a few configuration options for the plugin. All configurations are prefixed with grails.plugin.databasemigration:

Property Default Meaning

changelogLocation

grails-app/migrations

the folder containing the main changelog file (which can include one or more other files)

changelogFileName

changelog.groovy

the name of the main changelog file

changelogProperties

none

a map of properties to use for property substitution in Groovy DSL changelogs

contexts

none

A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

dbDocLocation

target/dbdoc

the directory where the output from the dbm-db-doc script is written

dbDocController.enabled

true in dev mode

whether the /dbdoc/ url is accessible at runtime

dropOnStart

false

if true then drops all tables before auto-running migrations (if updateOnStart is true)

updateOnStart

false

if true then changesets from the specified list of names will be run at startup

updateOnStartFileName

none

the file name (relative to changelogLocation) to run at startup if updateOnStart is true

updateOnStartDefaultSchema

none

the default schema to use when running auto-migrate on start

updateOnStartContexts

none

A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

updateAllOnStart

false

if true then changesets from the specified list of names will be run at startup for all dataSources. Useful for Grails Multitenancy with Multiple Databases (same db schema)

autoMigrateScripts

[RunApp]

the scripts when running auto-migrate. Useful to run auto-migrate during test phase with: [RunApp, TestApp]

excludeObjects

none

A comma-delimited list of database object names to ignore while performing a dbm-gorm-diff or dbm-generate-gorm-changelog

includeObjects

none

A comma-delimited list of database object names to look for while performing a dbm-gorm-diff or dbm-generate-gorm-changelog

databaseChangeLogTableName

databasechangelog

the Liquibase changelog record table name

databaseChangeLogLockTableName

databasechangeloglock

the Liquibase lock table name

All the above configs can be used for multiple datasources

Multiple DataSource Example:

If secondary dataSource named "second" is configured in application.yml

# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

grails:
    plugin:
        databasemigration:
            updateOnStart: true
            second:
                updateOnStart: true
---
server:
    port: 0
---
dataSource:
    pooled: true
    jmxExport: true
    driverClassName: org.h2.Driver

The configuration for this data source would be:

grails.plugin.databasemigration.reports.updateOnStart = true
grails.plugin.databasemigration.reports.changelogFileName = changelog-second.groovy

The configuration for all data sources with same db schema would be:

grails.plugin.databasemigration.updateAllOnStart = true
grails.plugin.databasemigration.changelogFileName = changelog.groovy

56.4. General Usage

After creating the initial changelog, the typical workflow will be along the lines of:

  • make domain class changes that affect the schema

  • add changes to the changelog for them

  • backup your database in case something goes wrong

  • run grails dbm-update to update your development environment (or wherever you’re applying the changes)

  • check the updated domain class(es) and changelog(s) into source control

  1. When running migration scripts on non-development databases, it’s important that you backup the database before running the migration in case anything goes wrong. You could also make a copy of the database and run the script against that, and if there’s a problem the real database will be unaffected.

  2. Setting the dbCreate setting to "none" is recommended when executing the dbm migration commands. Otherwise you might run into troubles and the commands could not be executed.

To create the changelog additions, you can either manually create the changes or with the dbm-gorm-diff script (you can also use the dbm-diff script but it’s far less convenient and requires a 2nd temporary database).

You have a few options with dbm-gorm-diff:

  • dbm-gorm-diff will dump to the console if no filename is specified, so you can copy/paste from there

  • if you include the --add parameter when running the script with a filename it will register an include for the filename in the main changelog for you

Regardless of which approach you use, be sure to inspect generated changes and adjust as necessary.

56.4.1. Autorun on start

Since Liquibase maintains a record of changes that have been applied, you can avoid manually updating the database by taking advantage of the plugin’s auto-run feature. By default this is disabled, but you can enable it by adding

grails.plugin.databasemigration.updateOnStart = true

to application.groovy. In addition you must specify the file containing changes; specify the name using the updateOnStartFileName property, e.g.:

grails.plugin.databasemigration.updateOnStartFileName = 'changelog.groovy'

Since changelogs can contain changelogs you’ll most often just specify the root changelog, changelog.groovy by convention. Any changes that haven’t been executed (in the specified file(s) or files included by them) will be run in the order specified.

You may optionally limit the plugin’s auto-run feature to run only specific contexts. If this configuration parameter is empty or omitted, all contexts will be run.

grails.plugin.databasemigration.updateOnStartContexts = ['context1,context2']

You can be notified when migration are run (for example to do some work before and/or after the migrations execute) by registering a "callback" class as a Spring bean. The class can have any name and package and doesn’t have to implement any interface since its methods will be called using Groovy duck-typing.

The bean name is "migrationCallbacks" and there are currently three callback methods supported (all are optional):

  • beforeStartMigration will be called (if it exists) for each datasource before any migrations have run; the method will be passed a single argument, the Liquibase Database for that datasource

  • onStartMigration will be called (if it exists) for each migration script; the method will be passed three arguments, the Liquibase Database, the Liquibase instance, and the changelog file name

  • afterMigrations will be called (if it exists) for each datasource after all migrations have run; the method will be passed a single argument, the Liquibase Database for that datasource

An example class will look like this:

package com.mycompany.myapp

import liquibase.Liquibase
import liquibase.database.Database

class MigrationCallbacks {

   void beforeStartMigration(Database Database) {
      ...
   }

   void onStartMigration(Database database, Liquibase liquibase, String changelogName) {
      ...
   }

   void afterMigrations(Database Database) {
      ...
   }
}

Register it in resources.groovy:

import com.mycompany.myapp.MigrationCallbacks

beans = {
   migrationCallbacks(MigrationCallbacks)
}

56.5. Groovy Changes

In addition to the built-in Liquibase changes (see the documentation for what’s available) you can also make database changes using Groovy code (as long as you’re using the Groovy DSL file format). These changes use the grailsChange tag name and are contained in a changeSet tag like standard built-in tags.

There are four supported inner tags and two callable methods (to override the default confirmation message and checksum value).

56.5.1. General format

This is the general format of a Groovy-based change; all inner tags and methods are optional:

databaseChangeLog = {

   changeSet(author: '...', id: '...') {

      grailsChange {
         init {
             // arbitrary initialization code; note that no
             // database or connection is available
         }

         validate {
            // can call warn(String message) to log a warning
            // or error(String message) to stop processing
         }

         change {
            // arbitrary code; make changes directly and/or return a
            // SqlStatement using the sqlStatement(SqlStatement sqlStatement)
            // method or multiple with sqlStatements(List sqlStatements)

            confirm 'change confirmation message'
         }

         rollback {
            // arbitrary code; make rollback changes directly and/or
            // return a SqlStatement using the sqlStatement(SqlStatement sqlStatement)
            // method or multiple with sqlStatements(List sqlStatements)

            confirm 'rollback confirmation message'
         }

         confirm 'confirmation message'

         checkSum 'override value for checksum'
      }

   }
}

56.5.2. Available variables

These variables are available throughout the change closure:

  • changeSet - the current Liquibase ChangeSet instance

  • resourceAccessor - the current Liquibase ResourceAccessor instance

  • ctx - the Spring ApplicationContext

  • application - the GrailsApplication

The change and rollback closures also have the following available:

  • database - the current Liquibase Database instance

  • databaseConnection - the current Liquibase DatabaseConnection instance, which is a wrapper around the JDBC Connection (but doesn’t implement the Connection interface)

  • connection - the real JDBC Connection instance (a shortcut for database.connection.wrappedConnection)

  • sql - a groovy.sql.Sql instance which uses the current connection and can be used for arbitrary queries and updates

init

This is where any optional initialization should happen. You can’t access the database from this closure.

validate

If there are any necessary validation checks before executing changes or rollbacks they should be done here. You can log warnings by calling warn(String message) and stop processing by calling error(String message). It may make more sense to use one or more preConditions instead of directly validating here.

change

All migration changes are done in the change closure. You can make changes directly (using the sql instance or the connection) and/or return one or more SqlStatements. You can call sqlStatement(SqlStatement statement) multiple times to register instances to be run. You can also call the sqlStatements(statements) method with an array or list of instances to be run.

rollback

All rollback changes are done in the rollback closure. You can make changes directly (using the sql instance or the connection) and/or return one or more SqlStatements. You can call sqlStatement(SqlStatement statement) multiple times to register instances to be run. You can also call the sqlStatements(statements) method with an array or list of instances to be run.

confirm

The confirm(String message) method is used to specify the confirmation message to be shown. The default is "Executed GrailsChange" and it can be overridden in the change or rollback closures to allow phase-specific messages or outside of both closures to use the same message for the update and rollback phase.

checkSum

The checksum for the change will be generated automatically, but if you want to override the value that gets hashed you can specify it with the checkSum(String value) method.

56.6. Groovy Preconditions

In addition to the built-in Liquibase preconditions (see the documentation for what’s available) you can also specify preconditions using Groovy code (as long as you’re using the Groovy DSL file format). These changes use the grailsPrecondition tag name and are contained in the databaseChangeLog tag or in a changeSet tag like standard built-in tags.

56.6.1. General format

This is the general format of a Groovy-based precondition:

databaseChangeLog = {

   changeSet(author: '...', id: '...') {

      preConditions {

         grailsPrecondition {

            check {

               // use an assertion
               assert x == x

               // use an assertion with an error message
               assert y == y : 'value cannot be 237'

               // call the fail method
               if (x != x) {
                  fail 'x != x'
               }

               // throw an exception (the fail method is preferred)
               if (y != y) {
                  throw new RuntimeException('y != y')
               }
            }

         }

      }
   }
}

As you can see there are a few ways to indicate that a precondition wasn’t met:

  • use a simple assertion

  • use an assertion with a message

  • call the fail(String message) method (throws a PreconditionFailedException)

  • throw an exception (shouldn’t be necessary - use assert or fail() instead)

56.6.2. Available variables

  • database - the current Liquibase Database instance

  • databaseConnection - the current Liquibase DatabaseConnection instance, which is a wrapper around the JDBC Connection (but doesn’t implement the Connection interface)

  • connection - the real JDBC Connection instance (a shortcut for database.connection.wrappedConnection)

  • sql - a groovy.sql.Sql instance which uses the current connection and can be used for arbitrary queries and updates

  • resourceAccessor - the current Liquibase ResourceAccessor instance

  • ctx - the Spring ApplicationContext

  • application - the GrailsApplication

  • changeSet - the current Liquibase ChangeSet instance

  • changeLog - the current Liquibase DatabaseChangeLog instance

56.6.3. Utility methods

  • createDatabaseSnapshotGenerator() - retrieves the DatabaseSnapshotGenerator for the current Database

  • createDatabaseSnapshot(String schemaName = null) - creates a DatabaseSnapshot for the current Database (and schema if specified)

56.7. GORM Support

The plugin’s support for GORM is one feature that differentiates it from using Liquibase directly. Typically, when using Liquibase you make changes to a database yourself, and then create changesets manually, or use a diff script to compare your updated database to one that hasn’t been updated yet. This is a decent amount of work and is rather error-prone. It’s easy to forget some changes that aren’t required but help performance, for example creating an index on a foreign key when using MySQL.

create-drop, create, and update

On the other end of the spectrum, Hibernate’s create-drop mode (or create) will create a database that matches your domain model, but it’s destructive since all previous data is lost when it runs. This works well in the very early stages of development but gets frustrating quickly. Unfortunately Hibernate’s update mode seems like a good compromise since it only makes changes to your existing schema, but it’s very limited in what it will do. It’s very pessimistic and won’t make any changes that could lose data. So it will add new tables and columns, but won’t drop anything. If you remove a not-null domain class property you’ll find you can’t insert anymore since the column is still there. And it will create not-null columns as nullable since otherwise existing data would be invalid. It won’t even widen a column e.g. from VARCHAR(100) to VARCHAR(200).

dbm-gorm-diff

The plugin provides a script that will compare your GORM current domain model with a database that you specify, and the result is a Liquibase changeset - dbm-gorm-diff. This is the same changeset you would get if you exported your domain model to a scratch database and diffed it with the other database, but it’s more convenient.

So a good workflow would be:

  • make whatever domain class changes you need (add new ones, delete unneeded ones, add/change/remove properties, etc.)

  • once your tests pass, and you’re ready to commit your changes to source control, run the script to generate the changeset that will bring your database back in line with your code

  • add the changeset to an existing changelog file, or use the include tag to include the whole file

  • run the changeset on your functional test database

  • assuming your functional tests pass, check everything in as one commit

  • the other members of your team will get both the code and database changes when they next update, and will know to run the update script to sync their database with the latest code

  • once you’re ready to deploy to QA for testing (or staging or production), you can run all the un-run changes since the last deployment

dbm-generate-gorm-changelog

The dbm-generate-gorm-changelog script is useful for when you want to switch from create-drop mode to doing proper migrations. It’s not very useful if you already have a database that’s in sync with your code, since you can just use the dbm-generate-changelog script that creates a changelog from your database.

56.8. DbDoc Controller

You can use the dbm-db-doc script to generate static HTML files to view changelog information, but another option is to use the DbDocController at runtime. By default this controller is mapped to /appname/dbdoc/ but this can be customized with UrlMappings like any controller.

You probably don’t want to expose this information to all of your application’s users so by default the controller is only enabled in the development environment. But you can enable or disable it for any environment in application.groovy with the dbDocController.enabled config option. For example to enable for all environments (be sure to guard the URL with a security plugin in prod):

grails.plugin.databasemigration.dbDocController.enabled = true

or to enable in the production environment:

environments {
   production {
      grails.plugin.databasemigration.dbDocController.enabled = true
   }
   ...
}

56.9. Reference

56.9.1. Diff Scripts

dbm-diff
Purpose

Compares two databases and creates a changelog that will make the changes required to bring them into sync.

Description

Executes against the database configured in application.[yml|groovy] for the current environment (defaults to dev) and another configured datasource in application.[yml|groovy].

If a filename parameter is specified then the output will be written to the named file, otherwise it will be written to the console. If the filename ends with .groovy a Groovy DSL file will be created, otherwise a standard XML file will be created.

File are written to the migrations folder, so specify the filename relative to the migrations folder (grails-app/migrations by default).

Usage:

grails <<environment>> dbm-diff <<otherEnv>> <<filename>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>> --add

Required arguments:

  • otherEnv - The name of the environment to compare to

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • defaultSchema - The default schema name to use

  • add - If specified add an include in the root changelog file referencing the new file

  • dataSource - If provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the defaultSchema and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-diff "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-gorm-diff
Purpose

Diffs GORM classes against a database and generates a changelog XML or Groovy DSL file.

Description

Creates a Groovy DSL file if the filename is specified and it ends with .groovy. If another extension is specified it creates a standard Liquibase XML file, and if no filename is specified it writes to the console.

File are written to the migrations folder, so specify the filename relative to the migrations folder (grails-app/migrations by default).

Similar to dbm-diff but diffs the current configuration based on the application’s domain classes with the database configured in application.[yml|groovy] for the current environment (defaults to dev).

Doesn’t modify any existing files - you need to manually merge the output into the changeset along with any necessary modifications.

You can configure database objects to be ignored by this script - either in the GORM classes or in the target database. For example you may want domain objects that are transient, or you may have externally-managed tables, keys, etc. that you want left alone by the diff script. The configuration name for these ignored objects is grails.plugin.databasemigration.ignoredObjects, whose value is a list of strings.

Usage:

grails <<environment>> dbm-gorm-diff <<filename>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>> --add

Required arguments: none .

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • defaultSchema - The default schema name to use

  • add - if specified add an include in the root changelog file referencing the new file

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the defaultSchema and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-gorm-diff "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports

56.9.2. Documentation Scripts

dbm-db-doc
Purpose

Generates Javadoc-like documentation based on current database and change log.

Description

Writes to the folder specified by the destination parameter, or to the grails.plugin.databasemigration.dbDocLocation configuration option (defaults to target/dbdoc).

Usage:

grails <<environment>> dbm-db-doc <<destination>> --contexts=<<contexts>> --dataSource=<<dataSource>>

Required arguments: none .

Optional arguments:

  • destination - The path to write to

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be included

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-db-doc "--contexts=<<contexts>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports

56.9.3. Maintenance Scripts

dbm-add-migration
Purpose

Adds a template migration file to your project and to the changelog file.

Description

This script provides a template in which to place your migration behaviour code, whether Grails code or raw SQL.

Usage:

grails <<environment>> dbm-add-migration <<migrationName>>

Required arguments:

  • migrationName - The name of the migration - will be used as a filename and the default migration id.

This script only supports .groovy-style migrations at the moment.
dbm-changelog-sync-sql
Purpose

Writes the SQL that will mark all changes as executed in the database to STDOUT or a file.

Description

Generates the SQL statements for the Liquibase DATABASECHANGELOG control table.

Usage:

grails <<environment>> dbm-changelog-sync-sql <<filename>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none.

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be included

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-changelog-sync "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter if the data source is configured as reports underneath the dataSources key in application.[yml|groovy] the suffix of reports will be used as the parameter value.
--dataSource=reports
dbm-changelog-sync
Purpose

Mark all changes as executed in the database.

Description

Registers all changesets as having been run in the Liquibase control table. This is useful when the changes have already been applied, for example if you’ve just created a changelog from your database using the dbm-generate-changelog script.

Usage:

grails <<environment>> dbm-changelog-sync --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none.

Optional arguments:

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be included

  • defaultSchema - The default schema name to use

  • dataSource - If provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-changelog-sync "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-changelog-to-groovy
Purpose

Converts a Liquibase XML changelog file to a Groovy DSL file.

Description

If the Groovy file name isn’t specified the name and location will be the same as the original XML file with a .groovy extension.

Usage:

grails <<environment>> dbm-changelog-to-groovy [xml_file_name] [groovy_file_name]

Required arguments:

  • xml_file_name - The name and path of the XML file to convert

Optional arguments:

  • groovy_file_name - The name and path of the Groovy file

dbm-clear-checksums
Purpose

Removes current checksums from database. On next run checksums will be recomputed.

Description

Usage:

grails <<environment>> dbm-clear-checksums --dataSource=<<dataSource>>

Required arguments: none.

Optional arguments:

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-clear-checksums  "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-create-changelog
Purpose

Creates an empty changelog file.

Description

Creates a new empty file instead of generating the file from the database (using dbm-generate-changelog) or your GORM classes (using dbm-generate-gorm-changelog).

Usage:

grails <<environment>> dbm-create-changelog <<filename>> --dataSource=<<dataSource>>

Required arguments:

  • filename - The path to the output file to write to

Optional arguments:

  • dataSource - if provided will run the script for the specified dataSource creating a file named changelog-<<dataSource>>.groovy if a filename is not given. Not needed for the default dataSource.

Note that the dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-create-changelog "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-drop-all
Purpose

Drops all database objects owned by the user.

Description

Usage:

grails <<environment>> dbm-drop-all <<schemaNames>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none.

Optional arguments:

  • schemaNames - A comma-delimited list of schema names to use

  • defaultSchema - The default schema name to use if the schemaNames parameter isn’t present

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the defaultSchema and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-drop-all "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-list-locks
Purpose

Lists who currently has locks on the database changelog to STDOUT or a file.

Description

Usage:

grails <<environment>> dbm-list-locks <<filename>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none.

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the defaultSchema and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-list-locks "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-list-tags
Purpose

Lists the tags in the current database.

Description

Usage:

grails <<environment>> dbm-list-tags --defaultSchema=<<defaultSchema>>

Required arguments:

Required arguments: none.

Optional arguments:

  • defaultSchema - The default schema name to use

Note that the defaultSchema parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-tag "--defaultSchema=<<defaultSchema>>"
dbm-mark-next-changeset-ran
Purpose

Mark the next change set as executed in the database.

Description

If a filename is specified, writes the SQL that will perform the update that file but doesn’t update.

Usage:

grails <<environment>> dbm-mark-next-changeset-ran <<filename>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none.

Optional arguments:

  • filename - The path to the output file to write to

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-mark-next-changeset-ran "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-release-locks
Purpose

Releases all locks on the database changelog.

Description

Usage:

grails <<environment>> dbm-release-locks --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none.

Optional arguments:

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the defaultSchema and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-release-locks "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-status
Purpose

Outputs count or list of unrun change sets to STDOUT or a file.

Description

Usage:

grails <<environment>> dbm-status <<filename>> --verbose=<<verbose>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none.

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • verbose - If true (the default) the changesets are listed; if false only the count is displayed

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be included

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the verbose, contexts, defaultSchema and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-status "--verbose=<<verbose>>" "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-tag
Purpose

Adds a tag to mark the current database state.

Description

Useful for future rollbacks to a specific tag (e.g. using the dbm-rollback script).

Usage:

grails <<environment>> dbm-tag <<tagName>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments:

  • tagName - The name of the tag to use

Optional arguments:

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the defaultSchema and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-tag "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-validate
Purpose

Checks the changelog for errors.

Description

Prints any validation messages to the console.

Usage:

grails <<environment>> dbm-validate --dataSource=<<dataSource>>

Required arguments: none.

Optional arguments:

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-validate "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports

56.10. Rollback Scripts

dbm-future-rollback-sql
Purpose

Writes SQL to roll back the database to the current state after the changes in the changeslog have been applied to STDOUT or a file.

Description

Usage:

grails <<environment>> dbm-future-rollback-sql <<filename>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none .

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be included

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-future-rollback-sql "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-generate-changelog
Purpose

Generates an initial changelog XML or Groovy DSL file from the database.

Description

Creates a Groovy DSL file if the filename is specified and it ends with .groovy. If another extension is specified it creates a standard Liquibase XML file, and if no filename is specified it writes to the console.

File are written to the migrations folder, so specify the filename relative to the migrations folder (grails-app/migrations by default).

Executes against the database configured in application.[yml|groovy] for the current environment (defaults to dev).

Usage:

grails <<environment>> dbm-generate-changelog <<filename>> --diffTypes=<<diffTypes>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>> --add

Required arguments: none .

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • diffTypes - A comma-delimited list of change types to include - see the documentation for what types are available

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

  • add - if specified add an include in the root changelog file referencing the new file

Note that the diffTypes, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-generate-changelog "--diffTypes=<<diffTypes>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-generate-gorm-changelog
Purpose

Generates an initial changelog XML or Groovy DSL file from current GORM classes.

Description

Creates a Groovy DSL file if the filename is specified and it ends with .groovy. If another extension is specified it creates a standard Liquibase XML file, and if no filename is specified it writes to the console.

File are written to the migrations folder, so specify the filename relative to the migrations folder (grails-app/migrations by default).

Executes against the database configured in DataSource.groovy for the current environment (defaults to dev).

Usage:

grails <<environment>> dbm-generate-gorm-changelog <<filename>> --dataSource=<<dataSource>> --add

Required arguments: none .

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

  • add - if specified add an include in the root changelog file referencing the new file

Note that the dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-generate-gorm-changelog  "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-rollback-count-sql
Purpose

Writes the SQL to roll back the specified number of change sets to STDOUT or a file.

Description

Usage:

grails <<environment>> dbm-rollback-count-sql <<number>> <<filename>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments:

  • number - The number of changesets to roll back

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be included

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-rollback-count-sql "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-rollback-count
Purpose

Rolls back the specified number of change sets

Description

Usage:

grails <<environment>> dbm-rollback-count <<number>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments:

  • number - The number of changesets to roll back

Optional arguments:

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-rollback-count "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-rollback-sql
Purpose

Writes SQL to roll back the database to the state it was in when the tag was applied to STDOUT or a file.

Description

Requires that the named tag exists. You can create tags with the dbm-tag script.

Usage:

grails <<environment>> dbm-rollback-sql <<tagName>> <<filename>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments:

  • tagName - The name of the tag to use

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be included

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-rollback-sql "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" --dataSource=<<dataSource>>
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-rollback-to-date-sql
Purpose

Writes SQL to roll back the database to the state it was in at the given date/time to STDOUT or a file.

Description

You can specify just the date, or the date and time. The date format must be yyyy-MM-dd and the time format must be HH:mm:ss.

Usage:

grails <<environment>> dbm-rollback-to-date-sql <<date>> <<time>> <<filename>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments:

  • date - The rollback date

Optional arguments:

  • time - The rollback time

  • filename - The path to the output file to write to. If not specified output is written to the console

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be included

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-rollback-to-date-sql "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-rollback-to-date
Purpose

Rolls back the database to the state it was in at the given date/time.

Description

You can specify just the date, or the date and time. The date format must be yyyy-MM-dd and the time format must be HH:mm:ss.

Usage:

grails <<environment>> dbm-rollback-to-date <<date>> <<time>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments:

  • date - The rollback date

Optional arguments:

  • time - The rollback time

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be included

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-rollback-to-date "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-rollback
Purpose

Rolls back the database to the state it was in when the tag was applied.

Description

Requires that the named tag exists. You can create tags with the dbm-tag script.

Usage:

grails <<environment>> dbm-rollback <<tagName>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments:

  • tagName - The name of the tag to use

Optional arguments:

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-rollback "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports

56.10.10. Update Scripts

dbm-previous-changeset-sql
Purpose

Writes the SQL to STDOUT or a file for the specified number of previous changesets whether they have run or not.

Description

Generates SQL for the specifed number of changeSets from the changelog. Executes against the database configured in application.[yml|groovy] for the current environment (defaults to dev).

Usage:

grails <<environment>> dbm-previous-changeset-sql <<number>> <<filename>> --skip=<<skip>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>>

Required arguments:

  • number - The number of un-run changesets to run

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • skip - The number of changesets to skip if you want to exclude recent ones

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

  • defaultSchema - The default schema name to use

Note that the contexts and defaultSchema parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-update-count-sql "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>"
dbm-update-count-sql
Purpose

Writes the SQL that will partially update a database to STDOUT or a file.

Description

Generates SQL for the specifed number of changeSets from the changelog. Executes against the database configured in application.[yml|groovy] for the current environment (defaults to dev).

This is useful for inspecting the generated SQL before running an update, or to generate SQL which can be tuned before running manually.

Usage:

grails <<environment>> dbm-update-count-sql <<number>> <<filename>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments:

  • number - The number of un-run changesets to run

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-update-count-sql "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-update-count
Purpose

Partially updates a database.

Description

Runs the specifed number of un-run changesets from the Changelog. Executes against the database configured in application.[yml|groovy] for the current environment (defaults to dev).

Usage:

grails <<environment>> dbm-update-count <<number>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments:

  • number - The number of un-run changesets to run

Optional arguments:

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-update-count "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-update-sql
Purpose

Writes the SQL that will update the database to the current version to STDOUT or a file.

Description

Generates SQL for all un-run changeSets from the changelog. Executes against the database configured in application.[yml|groovy] for the current environment (defaults to dev).

This is useful for inspecting the generated SQL before running an update, or to generate SQL which can be tuned before running manually.

Usage:

grails <<environment>> dbm-update-sql <<filename>> --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none .

Optional arguments:

  • filename - The path to the output file to write to. If not specified output is written to the console

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts, defaultSchema, and dataSource parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-update-sql "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" --dataSource=<<dataSource>>
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports
dbm-update
Purpose

Updates a database to the current version.

Description

Runs all un-run changeSets from the changelog. Executes against the database configured in application.[yml|groovy] for the current environment (defaults to dev).

Usage:

grails <<environment>> dbm-update --contexts=<<contexts>> --defaultSchema=<<defaultSchema>> --dataSource=<<dataSource>>

Required arguments: none .

Optional arguments:

  • contexts - A comma-delimited list of context names. If specified, only changesets tagged with one of the context names will be run

  • defaultSchema - The default schema name to use

  • dataSource - if provided will run the script for the specified dataSource. Not needed for the default dataSource.

Note that the contexts and defaultSchema parameter name and value must be quoted if executed in Windows, e.g.
grails dbm-update "--contexts=<<contexts>>" "--defaultSchema=<<defaultSchema>>" "--dataSource=<<dataSource>>"
For the dataSource parameter, if the data source is configured as reports underneath the dataSources key in application.[yml|groovy], the value should be reports.
--dataSource=reports