Hibernate Sessions and Transaction Management Guidelines
Manual Transaction Management in Plugins
The plugin system currently uses a different version of Spring (2.5.6) to the version shipped with Confluence (2.0.8). For this reason, there is a transaction abstraction provided by SAL that should be used for manual transaction management in plugins:
The type of the
transactionTemplate variable is
com.atlassian.sal.api.transaction.TransactionTemplate, and you can get this dependency-injected into your component.
Unlike the direct Spring transaction management, you cannot set a custom propagation behaviour or other transaction attributes. The implementation of SAL TransactionTemplate in Confluence always uses
PROPAGATION_REQUIRED and marks the transaction as read-write.
Transaction Management in Confluence
Transaction demarcation is provided by Spring, with a few wrinkles.
- We wrap managers in transaction interceptors, but not DAOs.
- We use whatever the default isolation level is for whatever database we're connecting to
- We commit the transaction manually between performing an action, and displaying the view.
The last point is necessary because in some cases, we were sending redirect responses to the browser then committing the transaction. A quick browser would request the redirect page before their transaction was committed, and view stale data as a result. By committing the transaction before we render the view, we make sure that everything we expect to be in the database is in the database before the browser has a chance to re-request it.
Manual Transaction Management
While you can normally use transaction interceptors configured through Spring, occasionally there is a need to programmatically initialise and commit a transaction. You can use the Spring
TransactionTemplate to do so, as shown in the following example.
The type of the
transactionManager field in this example is
org.springframework.transaction.PlatformTransactionManager. You can get this injected by Spring into your component.
The propagation behaviour of your transaction should normally be PROPAGATION_REQUIRED. This will join an existing transaction if one is present, or otherwise start a new one. Marking a transaction as read-only will help the performance of Hibernate by avoiding unnecessary dirty checks on objects, if there isn't an existing read-write transaction in progress.
You can read more about the other propagation and transaction options in the Spring documentation.
Sessions are a Hibernate construct used to mediate connections with the database.
The session opens a single database connection when it is created, and holds onto it until the session is closed. Every object that is loaded by Hibernate from the database is associated with the session, allowing Hibernate to automatically persist objects that are modified, and allowing Hibernate to implement functionality such as lazy-loading.
If an object is evicted from its session (for example via a clear, see below), or the session is closed while the object is still kept alive, the object is "disconnected" from the session. A disconnected object will continue to work so long as you don't perform any operation that it needs to go back to the database for, such as accessing a lazily-loaded collection.
If you see a LazyInitializationException, it means that a Hibernate-managed object has lived longer than its session.
Managed objects are not portable between sessions. Trying to load an object in one session then save it into another session will also result in an error. (You can use
Session.get() to re-introduce an object to a new session, but you're much better off fixing whatever problem is causing you to try to move objects between sessions in the first place.
Storing hibernate objects in caches is a bad idea. By definition, a hibernate-managed object placed in a cache will outlive its session. Even if caching such an object is safe now, it's quite likely that in the future we might switch some of its properties to be lazily-loaded, or change code-paths so that properties that were previously being loaded before the object was cached aren't being loaded any more. The
LazyInitializationException errors that result rarely show up in tests, and are hard to diagnose and fix.
Hibernate maintains its own second-level cache that does not suffer from this problem. Use it in preference to manually caching Hibernate data.
If you need to cache information from Hibernate, don't cache the Hibernate objects themselves. A useful alternative is to cache the object's ID and class, and then retrieve the object in the context of the current session using
Session.get(class, id). ID lookups go straight through Hibernate's own second-level cache, so are (hopefully) efficient. The
findByHandle() methods of the
AnyTypeObjectDao provide a helpful API for doing just this.
Flushing and Clearing
When the session persists its changes to the database, this is called "flushing". During a flush, each object associated with the session is checked to see if it has changed state. Any object with changed state will be persisted to the database, regardless of whether the changed objects are explicitly saved or not. You can configure Hibernate's flush behaviour, but the default (
FlushMode.AUTO) will flush the session:
- When you manually call
flush()on the session
- Before Hibernate performs a query, if Hibernate believes flushing is necessary for the query to get accurate results
- When a transaction is committed
- When the session is closed.
How long a flush takes is a function of the number of objects associated with the session. Thus, the more objects you load during the lifetime of a session, the less efficient each query will be (as a flush will generally be done prior to each query). If you have some long-running operation that gets slower and slower and slower as it runs, it's possible that the Hibernate session is the cause.
Operations that cycle through large numbers of objects should follow our guidelines for bulk operations in Hibernate.
Hibernate sessions are not thread-safe. Not only does this mean you shouldn't pass a Hibernate session into a new thread, it also means that because objects you load from a session can be called from (and call back to) their owning session, you must not share Hibernate-managed objects between threads. Once again, try to only pass object IDs, and load the object freshly from the new thread's own session.
Spring's transaction management places the Hibernate session in a ThreadLocal variable, accessed via the
sessionFactory. All Confluence DAOs use that ThreadLocal. This means that when you create a new thread you no longer have access to the Hibernate session for that thread (a good thing, as above), and you are no longer part of your current transaction.
The Session In View Filter
Confluence uses the "Session in view" pattern for managing Hibernate sessions. The
SessionInViewFilter opens a Hibernate session which is then available for the entire web request. The advantages of this is that you avoid session-related errors:
- The session lifecycle is uniform for every request
- Hibernate objects remain "alive" for the whole request, thus you can still retrieve lazily-loaded data in Velocity templates
The disadvantages are:
- Each request monopolises a database connection from the moment a request comes in, to the last byte sent to the client
- Each session will end up associated with every object that is loaded for the duration of the request
- Developers are often caught out by the way sessions behave when threads haven't come in through the web tier (i.e. Quartz jobs)
Non-web requests do not automatically have a Hibernate session to work with, because they don't come in through the Session In View Filter. This includes start-up events, quartz jobs, and any long-running task that spawns a new thread. As a result, a new session will be opened when you make a call to a transaction-managed Spring object, and closed when that call returns.
A very common programming error in this context is to retrieve a collection of objects from a manager, then do something to each object. The moment the call to the manager returns, all objects will be detached from their containing session. If you try to do anything to them after that, you won't get the result you expected. I'm not sure if this sequence diagram helps, but here goes...
Consider moving such operations into the manager itself, so the whole operation will be wrapped in the one transaction. Alternatively, if making everything run in separate transactions is what you want, have the job retrieve a collection of IDs, and pass those back to the manager one by one.
Managing a Session Manually
In certain contexts, like a scheduled task in a plugin, you may want to create and manage a session manually. This can be done by using Spring's
HibernateTemplate as shown in the following example.
The type of the
sessionFactory field in this example is
net.sf.hibernate.SessionFactory. You can get this injected by Spring into your component.
This code will create a new session if one is not already bound to the thread, execute the callback code, then close the session and release the database connection back to the pool. Making direct calls to the SessionFactory is not recommended because it is very easy to leak database connections if the sessions are not closed properly.