Last updated Jun 12, 2024

Hibernate sessions and transaction management guidelines

Manual Transaction Management in Plugins

If the plugin system uses a different version of Spring to the version shipped with Confluence, there is a transaction abstraction provided by SAL that should be used for manual transaction management in plugins:

1
2
Object result = transactionTemplate.execute(new TransactionCallback()
{
    @Override
    public Object doInTransaction()
    {
        // ... execute transactional code ...

        return null;
    }
});

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.

1
2
TransactionDefinition transactionDefinition = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED);
new TransactionTemplate(transactionManager, transactionDefinition).execute(new TransactionCallback()
{
    @Override
    public Object doInTransaction(TransactionStatus status)
    {
        // ... execute transactional code ...

        return null;
    }
});

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.

Wrap method calls in a transactionTemplate

Calling Hibernate manager's method (such as AttachmentManager and PageManager) could cause a hibernate object to be in an inconsistent state. Method calls like page.saveNewVersion(…) and other hibernate manager mutation calls need to be wrapped in a transaction. For example:

1
2
transactionTemplate.execute((TransactionCallback<Void>) () -> {      
    // [...]
    attachmentManager.saveAttachment(...);
    // [...]
    Page page = pageManager.getPage(pageId);
    Page originalPage = (Page) page.clone();
    String content = page.getBodyAsString();
    //modify content
    page.setBodyAsString(content);
    pageManager.saveContentEntity(page, originalPage, null);     
    return null;
});

Failure to do this can result in exceptions, such as org.hibernate.StaleObjectStateException, due to the way Confluence tries to trigger content reconciliation after attachment changes. This change was introduced in Confluence 7.13.0, and backported to 7.4.9, 7.11.3, and 7.12.1.

By wrapping all the code within one transaction, we ensure all manager method calls use the same Transaction (as the default propagation level is PROPAGATION_REQUIRED). The stage of the Hibernate object (such as page) will be consistent across method call.

Best practice when working with Hibernate objects is for all of them to belong to single Unit of Work, see Section 11.1.1 Unit of Work in the Hibernate documentation. Although Confluence provides transaction wrapper in manager’s method call to make it easier to use our API, you should change your implementation to follow best practices and be more resilient.

Hibernate Sessions

Sessions are a Hibernate construct used to mediate connections with the database. From Confluence 8.0 direct access to Hibernate Sessions will be removed. Please refer to Confluence API docuemntation for available options.

Custom Hibernate Queries

Sometimes data provided from API is not enough and you need to perform a custom query to retrieve objects from the database. One way of doing this was to create a custom query using Hibernate Session. However, this option is now deprecated and it will be removed from Confluence in a future release. Instead, Confluence API provides an alternative way of extracting data using custom queries.

Follow this design:

  1. Create a factory class implementing HibernateContentQueryFactory. In getQuery method, use provided EntityManager object to create your query.
    1
    2
    public class FindAllCurrentAbstractPageIdsHibernateQueryFactory implements HibernateContentQueryFactory {
        @Override
        public Query getQuery(EntityManager entityManager, Object... parameters) {
            Query query = entityManager.createQuery(
                    "select page.id from AbstractPage page where page.space.id = :spaceId and page.contentStatus = 'current'"
            );
            // ... set query parameters if needed
            return query;
        }
    }
    
  2. In atlassian-plugin.xml file add the following entry inside the atlassian-plugin tag. It needs to have a unique query name and point to the class created in the previous step.
    1
    2
    <atlassian-plugin ... >
    ...
        <content-custom-query query-name="<your unique query name>" key="find-all-abstract-page-ids" 
            name="Custom Hibernate Query" class="com.your.project.FindAllCurrentAbstractPageIdsHibernateQueryFactory"/>
    
  3. Now you can use ContentQuery class to retrieve your query and pass it to CustomContentManager for execution inside your transaction.
    1
    2
    final Iterator<Long> getCurrentPageAndBlogPostIdsInSpace(Object... parameters) {
        return transactionTemplate.execute(() -> {
            final ContentQuery<Long> query =
                new ContentQuery<Long>("<your unique query name>", parameters);
            return customContentManager.findByQuery(query, 0, Integer.MAX_VALUE);
        });
    }
    

This example will retrieve list of page IDs. Please refer to CustomContentManager documentation for more customisation options.

Rate this page: