Book.withNewSession {
// your logic here
}
6 Persistence Basics
Version: 2023.3.0-SNAPSHOT
Table of Contents
6 Persistence Basics
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 to achieve that is with the withNewSession(Closure) method:
Another option is to bind a transaction using the withTransaction(Closure) method:
Book.withTransaction {
// your logic here
}
6.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 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.
6.2 Saving and Updating
An example of using the save() method can be seen below:
def p = Person.get(1)
p.save()
This save will be not be pushed to the database immediately - it will be pushed when the next flush occurs. But there are occasions when you want to control when those statements are executed or, in Hibernate terminology, when the session is "flushed". To do so you can use the flush argument to the save method:
def p = Person.get(1)
p.save(flush: true)
Note that in this case all pending SQL statements including previous saves, deletes, etc. will be synchronized with the database. This also lets you catch any exceptions, which is typically useful in highly concurrent scenarios involving optimistic locking:
def p = Person.get(1)
try {
p.save(flush: true)
}
catch (org.springframework.dao.DataIntegrityViolationException e) {
// deal with exception
}
Another thing to bear in mind is that GORM validates a domain instance every time you save it. If that validation fails the domain instance will not be persisted to the database. By default, save()
will simply return null
in this case, but if you would prefer it to throw an exception you can use the failOnError
argument:
def p = Person.get(1)
try {
p.save(failOnError: true)
}
catch (ValidationException e) {
// deal with exception
}
You can even change the default behaviour with a setting in application.groovy
, as described in the section on configuration. Just remember that when you are saving domain instances that have been bound with data provided by the user, the likelihood of validation exceptions is quite high and you won’t want those exceptions propagating to the end user.
6.3 Deleting Objects
An example of the delete() method can be seen below:
def p = Person.get(1)
p.delete()
As with saves, Hibernate will use transactional write-behind to perform the delete; to perform the delete in-place you can use the flush
argument:
def p = Person.get(1)
p.delete(flush: true)
Using the flush
argument lets you catch any errors that occur during a delete. A common error that may occur is if you violate a database constraint, although this is normally down to a programming or schema error. The following example shows how to catch a DataIntegrityViolationException
that is thrown when you violate the database constraints:
import org.springframework.dao.*
def p = Person.get(1)
try {
p.delete(flush: true)
}
catch (DataIntegrityViolationException e) {
// handle the error
}
In order to perform a batch delete there are a couple of ways to achieve that. One way is to use a Where Query:
Person.where {
name == "Fred"
}.deleteAll()
Another alternative is to use an HQL statement within the executeUpdate(…) method:
Customer.executeUpdate("delete Customer c where c.name = :oldName",
[oldName: "Fred"])
6.4 Understanding Cascading Updates and Deletes
It is critical that you understand how cascading updates and deletes work when using GORM. The key part to remember is the belongsTo
setting which controls which class "owns" a relationship.
Whether it is a one-to-one, one-to-many or many-to-many, defining belongsTo
will result in updates cascading from the owning class to its dependant (the other side of the relationship), and for many-/one-to-one and one-to-many relationships deletes will also cascade.
If you do not define belongsTo
then no cascades will happen and you will have to manually save each object (except in the case of the one-to-many, in which case saves will cascade automatically if a new instance is in a hasMany
collection).
Here is an example:
class Airport {
String name
static hasMany = [flights: Flight]
}
class Flight {
String number
static belongsTo = [airport: Airport]
}
If I now create an Airport
and add some Flight
s to it I can save the Airport
and have the updates cascaded down to each flight, hence saving the whole object graph:
new Airport(name: "Gatwick")
.addToFlights(new Flight(number: "BA3430"))
.addToFlights(new Flight(number: "EZ0938"))
.save()
Conversely if I later delete the Airport
all Flight
instances associated with it will also be deleted:
def airport = Airport.findByName("Gatwick")
airport.delete()
The above examples are called transitive persistence and are controlled via the belongsTo
and the cascade policy. If I were to remove belongsTo
then the above cascading deletion code would not work.
Unidirectional Many-To-One without belongsTo
For example, consider the following domain model:
class Location {
String city
}
class Author {
String name
Location location
}
It looks simple, right? And it is. Just set the location property to a Location instance and you have linked an author to a location. But see what happens when we run the following code:
def a = new Author(name: "Niall Ferguson", location: new Location(city: "Boston"))
a.save()
An exception is thrown. If you look at the ultimate “caused by” exception, you’ll see the message “not-null property references a null or transient value: Author.location”
. What’s going on?
A transient instance is one that isn’t attached to a Hibernate session. As you can see from the code, we are setting the Author.location property to a new Location instance, not one retrieved from the database. Hence the instance is transient. The obvious fix is to make the Location instance persistent by saving it:
def l = new Location(city: "Boston")
l.save()
def a = new Author(name: "Niall Ferguson", location: l)
a.save()
Another option is to alter the cascade policy for the association. There are two ways to do that. One way is to define belongsTo
on the Location
class:
class Location {
String city
static belongsTo = Author
}
Note that this above syntax does not make the association bidirectional since no property is defined. A bidirectional example would be:
class Location {
String city
static belongsTo = [author:Author]
}
Alternatively if you prefer that the Location
class has nothing to do with the Author
class you can define the cascade policy in Author
:
class Author {
String name
Location location
static mapping = {
location cascade:'save-update'
}
}
The above example will configure the cascade policy to cascade saves and updates, but not deletes.
Bidirectional one-to-many with belongsTo
class A { static hasMany = [bees: B] }
class B { static belongsTo = [a: A] }
In the case of a bidirectional one-to-many where the many side defines a belongsTo
then the cascade strategy is set to "ALL" for the one side and "NONE" for the many side.
What this means is that whenever an instance of A
is saved or updated. So will any instances of B
. And, critically, whenever any instance of A
is deleted so will all the associated instances of B
!
Unidirectional One-to-Many
class A { static hasMany = [bees: B] }
class B { }
In the case of a unidirectional one-to-many where the many side defines no belongsTo then the cascade strategy is set to "SAVE-UPDATE".
Since the belongsTo
is not defined, this means that saves and updates will be cascaded from A
to B
, however deletes will not cascade!
Only when you define belongsTo
in B
or alter the cascading strategy of A
will deletes be cascaded.
Bidirectional One-to-Many, no belongsTo
class A { static hasMany = [bees: B] }
class B { A a }
In the case of a bidirectional one-to-many where the many side does not define a belongsTo
then the cascade strategy is set to "SAVE-UPDATE" for the one side and "NONE" for the many side.
So exactly like the previous case of a undirectional One-to-Many, without belongsTo
definition no delete operations will be cascaded, but crucially saves and updates will by default. If you do not want saves and updates to cacade then you must alter the cascade policy of A
:
class A {
static hasMany = [bees: B]
static mapping = {
bees cascade:"none"
}
}
Unidirectional Many-to-One with belongsTo
class A { }
class B { static belongsTo = [a: A] }
In the case of a unidirectional many-to-one association that defines a belongsTo
then the cascade strategy is set to "ALL" for the owning side of the relationship (A→B) and "NONE" from the side that defines the belongsTo
(B→A)
You may be wondering why this association is a many-to-one and not a one-to-one. The reason is because it is possible to have multiple instances of B
associated to the same instance of A
. If you wish to define this association as a true one-to-one association a unique
constraint is required:
class B {
static belongsTo = [a: A]
static constraints = {
a unique:true
}
}
Note that if you need further control over cascading behaviour, you can use the ORM DSL.
6.5 Eager and Lazy Fetching
Associations in GORM are by default lazy. This is best explained by example:
class Airport {
String name
static hasMany = [flights: Flight]
}
class Flight {
String number
Location destination
static belongsTo = [airport: Airport]
}
class Location {
String city
String country
}
Given the above domain classes and the following code:
def airport = Airport.findByName("Gatwick")
for (flight in airport.flights) {
println flight.destination.city
}
GORM will execute a single SQL query to fetch the Airport
instance, another to get its flights, and then 1 extra query for each iteration over the flights
association to get the current flight’s destination. In other words you get N+1 queries (if you exclude the original one to get the airport).
6.6 Configuring Eager Fetching
An alternative approach that avoids the N+1 queries is to use eager fetching, which can be specified as follows:
class Airport {
String name
static hasMany = [flights: Flight]
static mapping = {
flights lazy: false
}
}
In this case the flights
association will be loaded at the same time as its Airport
instance, although a second query will be executed to fetch the collection. You can also use fetch: 'join'
instead of lazy: false
, in which case GORM will only execute a single query to get the airports and their flights. This works well for single-ended associations, but you need to be careful with one-to-manys. Queries will work as you’d expect right up to the moment you add a limit to the number of results you want. At that point, you will likely end up with fewer results than you were expecting. The reason for this is quite technical but ultimately the problem arises from GORM using a left outer join.
So, the recommendation is currently to use fetch: 'join'
for single-ended associations and lazy: false
for one-to-manys.
Be careful how and where you use eager loading because you could load your entire database into memory with too many eager associations. You can find more information on the mapping options in the section on the ORM DSL.
6.7 Altering Fetch Strategy for a Query
Rather than configuring join fetching as the default for an association, it may be better to alter the join strategy only for the queries that require it. This can be done using the fetch
argument to most GORM methods:
// Using the list method
Author.list(fetch: [location: 'join']).each { a ->
println a.location.city
}
// Using a dynamic finder
Author.findAllByNameLike("John%", [ sort: 'name', order: 'asc', fetch: [location: 'join'] ]).each { a->
...
}
Or using the join
method when using Where Queries or criteria:
Author.where {
name == "Stephen King"
}.join('location')
.list()
6.8 Using Batch Fetching
Although eager fetching is appropriate for some cases, it is not always desirable. If you made everything eager you could quite possibly load your entire database into memory resulting in performance and memory problems. An alternative to eager fetching is to use batch fetching. You can configure Hibernate to lazily fetch results in "batches". For example:
class Airport {
String name
static hasMany = [flights: Flight]
static mapping = {
flights batchSize: 10
}
}
In this case, due to the batchSize
argument, when you iterate over the flights
association, Hibernate will fetch results in batches of 10. For example if you had an Airport
that had 30 flights, if you didn’t configure batch fetching you would get 1 query to fetch the Airport
and then 30
queries to fetch each flight. With batch fetching you get 1 query to fetch the Airport
and 3 queries to fetch each Flight
in batches of 10. In other words, batch fetching is an optimization of the lazy fetching strategy. Batch fetching can also be configured at the class level as follows:
class Flight {
...
static mapping = {
batchSize 10
}
}
6.9 Pessimistic and Optimistic Locking
Optimistic Locking
By default GORM classes are configured for optimistic locking. Optimistic locking is a feature of Hibernate which involves storing a version value in a special version
column in the database that is incremented after each update.
The version
column gets read into a version
property that contains the current versioned state of persistent instance which you can access:
def airport = Airport.get(10)
println airport.version
When you perform updates Hibernate will automatically check the version property against the version column in the database and if they differ will throw a StaleObjectException. This will roll back the transaction if one is active.
This is useful as it allows a certain level of atomicity without resorting to pessimistic locking that has an inherit performance penalty. The downside is that you have to deal with this exception if you have highly concurrent writes. This requires flushing the session:
def airport = Airport.get(10)
try {
airport.name = "Heathrow"
airport.save(flush: true)
}
catch (org.springframework.dao.OptimisticLockingFailureException e) {
// deal with exception
}
The way you deal with the exception depends on the application. You could attempt a programmatic merge of the data or go back to the user and ask them to resolve the conflict.
Alternatively, if it becomes a problem you can resort to pessimistic locking.
The version will only be updated after flushing the session.
|
Pessimistic Locking
Pessimistic locking is equivalent to doing a SQL "SELECT * FOR UPDATE" statement and locking a row in the database. This has the implication that other read operations will be blocking until the lock is released.
In GORM pessimistic locking is performed on an existing instance with the lock() method:
def airport = Airport.get(10)
airport.lock() // lock for update
airport.name = "Heathrow"
airport.save()
GORM will automatically deal with releasing the lock for you once the transaction has been committed.
However, in the above case what we are doing is "upgrading" from a regular SELECT to a SELECT..FOR UPDATE and another thread could still have updated the record in between the call to get()
and the call to lock()
.
To get around this problem you can use the static lock(id) method that takes an id just like get(id):
def airport = Airport.lock(10) // lock for update
airport.name = "Heathrow"
airport.save()
In this case only SELECT..FOR UPDATE is issued.
As well as the lock(id) method you can also obtain a pessimistic locking using queries. For example using a dynamic finder:
def airport = Airport.findByName("Heathrow", [lock: true])
Or using criteria:
def airport = Airport.createCriteria().get {
eq('name', 'Heathrow')
lock true
}
6.10 Modification Checking
Once you have loaded and possibly modified a persistent domain class instance, it isn’t straightforward to retrieve the original values. If you try to reload the instance using get(id) Hibernate will return the current modified instance from its Session cache.
Reloading using another query would trigger a flush which could cause problems if your data isn’t ready to be flushed yet. So GORM provides some methods to retrieve the original values that Hibernate caches when it loads the instance (which it uses for dirty checking).
isDirty
You can use the isDirty() method to check if any field has been modified:
def airport = Airport.get(10)
assert !airport.isDirty()
airport.properties = params
if (airport.isDirty()) {
// do something based on changed state
}
isDirty() does not currently check collection associations, but it does check all other persistent properties and associations.
|
You can also check if individual fields have been modified:
def airport = Airport.get(10)
assert !airport.isDirty()
airport.properties = params
if (airport.isDirty('name')) {
// do something based on changed name
}
isDirty and Proxies
Dirty checking uses the equals()
method to determine if a property has changed. In the case of associations, it is important to recognize that if the association is a proxy, comparing properties on the domain that are not related to the identifier will initialize the proxy, causing another database query.
If the association does not define equals()
method, then the default Groovy behavior of verifying the instances are the same will be used. Because proxies are not the same instance as an instance loaded from the database, which can cause confusing behavior. It is recommended to implement the equals()
method if you need to check the dirtiness of an association. For example:
class Author {
Long id
String name
/**
* This ensures that if either or both of the instances
* have a null id (new instances), they are not equal.
*/
@Override
boolean equals(o) {
if (!(o instanceof Author)) return false
if (this.is(o)) return true
Author that = (Author) o
if (id !=null && that.id !=null) return id == that.id
return false
}
}
class Book {
Long id
String title
Author author
}
getDirtyPropertyNames
You can use the getDirtyPropertyNames() method to retrieve the names of modified fields; this may be empty but will not be null:
def airport = Airport.get(10)
assert !airport.isDirty()
airport.properties = params
def modifiedFieldNames = airport.getDirtyPropertyNames()
for (fieldName in modifiedFieldNames) {
// do something based on changed value
}
getPersistentValue
You can use the getPersistentValue(fieldName) method to retrieve the value of a modified field:
def airport = Airport.get(10)
assert !airport.isDirty()
airport.properties = params
def modifiedFieldNames = airport.getDirtyPropertyNames()
for (fieldName in modifiedFieldNames) {
def currentValue = airport."$fieldName"
def originalValue = airport.getPersistentValue(fieldName)
if (currentValue != originalValue) {
// do something based on changed value
}
}