An Overview on Grails Transactions

Sérgio Lourenço
Engineering Samlino
5 min readDec 17, 2019

This is based on an internal training session presentation done in CompareEuropeGroup, adapted to this format.

Database transactions

Transactions are a fundamental concept of all database systems. The essential point of a transaction is that it bundles multiple steps into a single, all-or-nothing operation. The intermediate states between the steps are not visible to other concurrent transactions, and if some failure occurs that prevents the transaction from completing, then none of the steps affect the database at all.

from PostgreSQL Docs

Photo by Manuel Geissinger on Pexels

Although there are many details and functionalities to it, for the most common usages, a transaction just means either all commands succeed or none is executed. They allow us to maintain data integrity even on failures and unexpected errors of the application.

Imagine the following example:

Example Transaction

This is a transfer of 100 between 2 accounts (Bob and Alice). Image that there is a rule stating that no account can have a negative balance and Bob has less than 100 in his account. The second command would fail.

If these operations were not made within a transaction, we would have corrupted data — Alice would have an extra 100 that came from nowhere.

Within a transaction, as shown above, when the second command fails the first is rolled back. Alice never gets the extra 100, and the operation is marked as failed. We ensure data consistency — a failed operation produced no effects.

Hibernate

Hibernate is one of the most commonly used ORMs around and is available to be used on Grails applications. Like all ORMs, it serves as an abstraction layer on top of the database, to avoid running SQL queries manually. It implements the JDBC standard for database access and respects the JPA standard.

It has a multitude of default behaviors to improve the performance of the database queries (lazy initialization, optimistic locking) and has an internal cache managed by session to reduce the number of DB queries.

Overview of Hibernate Caches
  • First Level Cache — Unique per session. Stores result of queries done to reduce database accesses
  • Second Level Cache — Configured by class, allows to share cached objects between different sessions

Flush

Flush is an Hibernate command to force the queries being cached to run immediately, therefore synchronizing the connection with the in memory objects.

Hibernate and Transactions

As expected, hibernate supports the usage of transactions. Consider the following example:

Transactions in Hibernate

This code is the Java equivalent to the previous SQL queries. If any exception occurs during the balance changes, all changes will be rolled back and no data will be corrupted.

Grails Transactions

Depending on the version of Grails you use, the default behavior regarding transactions will change. In versions 3.0.x, all services are transactional by default, while in versions 3.1.x and above, transactional services have to be specified as such.

Transactions in Grails

Note that there are 2 transactional annotations available to use:

  • grails.transaction.Transactional — Grails transaction. This, unlike in Spring, is an AST Transformation and eliminates the need for a proxy.
  • org.springframework.transaction.annotation.Transactional — Spring default transaction, using proxies when no interface is involved. Read more at their doc.

This annotation will, more or less, introduce the Hibernate transaction code we saw before in the transactional methods.

What does flush do?

Calling domainObject.save(flush:true) will not execute a commit command when inside a transaction. It does not break the normal transaction flow.

Transactions and Multi Threading

When working with multiple threads and database queries (or transactions) at the same time, particular problems can occur. To avoid it, some things must be remembered:

  1. Each thread will have its own Hibernate session, which means that the cache will not be shared and may become outdated very quickly.
    This is because Hibernate might not commit to the database immediately (by default it manages it internally, although at the end of each session / thread it will flush everything). This can be controlled using flush or/and smaller transactions.
    Also, due to the cache, when reading data it might not reflect the latest changes in the database. Grails has available a refresh() method which will force an update of the internal cache.
  2. If optimistic locking is not enabled, you might have deadlocks. Admittedly, this will happen mostly because of poorly managed transactions. Image the following scenario:
Grails Transactions & Multi-Threading

Each of these two methods has a chance of blocking the other, even on different threads, because they might cause a lock on the database. Since the long-running service can last a long time, other method calls to the same id will be stuck until it finishes. If this is a highly accessed object, this will spell trouble fast.

Here, it is simple to see that the long-running service should not be executed inside the transaction, but in more complex scenarios and with default behaviors it might not be so clear where the problem is.

Transactions are a useful paradigm that help us maintain data consistency even when performing several complex operations. They have some overhead, as the management of transactions is a complex task. To help with managing transactions, ORMs like Hibernate, integrated with frameworks like Spring or Grails, provide multiple mechanisms to create transactions.

We need to keep it mind that the way transactions are managed can lead to certain headaches, like dead locks or out-of-sync applications. Having a good, even if simplified, knowledge of how transactions work in the tools that we use will help us avoid these problems.

--

--