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 2Object 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 demarcation is provided by Spring, with a few wrinkles.
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.
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 2TransactionDefinition 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.
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 2transactionTemplate.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.
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.
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:
HibernateContentQueryFactory
. In getQuery
method, use provided EntityManager
object to create your query.
1 2public 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; } }
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"/>
ContentQuery
class to retrieve your query and pass it to CustomContentManager
for execution inside your transaction.
1 2final 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: