Working with JCR repositories within the context of JTA transactions is quite straightforward. Your application, service or component still gets a Session as usual, and all read and write behaviors are unchanged. The only difference is when transient state within your session is persisted to the repository.
When not using transactions, calling Session.save() will immediately write the transient changes to the repository. When using transactions, however, JCR clients are still expected to call Session.save(), but only when the transaction is committed are the changes are persisted in the repository, visible to other sessions, and described by events.
Only the transient changes that were saved will be committed in the transaction. Don't forget to call Session.save(), even in code that relies on transactions.
Let's look at several examples of using the JCR API with transactions.
EJBs with container-managed transactions
The latest version of Java EE greatly simplifies EJBs while still providing all the same (if not more) benefits, like container-managed transactions. Let's consider this stateless EJB that stores a file inside a repository. To keep things simple (and focused on the transactions), we'll use a utility called JcrTools that ModeShape includes in its public API with a method to upload the content in the supplied InputStream into a nt:file node (with its "jcr:content" child node), creating any missing ancestor nt:folder nodes along the way:
ContentProvider.java
@Stateless
public class ContentProvider {
protected Session getSession() throws RepositoryException, NamingException {
InitialContext context = new InitialContext();
Repository repository = (Repository)context.lookup("jcr/my_repository");
return repository.login(); // uses default workspace and JAAS
}
public void uploadFile( String path, InputStream stream ) throws RepositoryException, NamingException, IOException {
Session session = getSession();
try {
// Use a ModeShape utility ...
new JcrTools().uploadFile(session,path,stream);
session.save();
} finally {
session.logout();
}
}
}
This EJB can then be called by any other component in the web application (including JSPs). And notice that we don't include any transaction-related code, yet this is what happens when the uploadFile method is called:
-
If a transaction doesn't exist, one will be started
-
The uploadFile(...) method is executed
-
A new Session is created using the current JAAS credentials (line 10)
-
nt:folder and nt:file nodes dictated by the path are created as needed, a Binary value is created with the content from the InputStream and set as the "jcr:data" property on the "jcr:content" child of the nt:file node (line 13)
-
The changes made to the session are saved (line 14)
-
The session is closed (line 16)
-
The transaction (started before the uploadFile method is executed or at an earlier point) is committed (or rolled back if an exception is thrown)
Now the somewhat tricky part. The transient changes made on line 13 are not persisted when the Session is saved (on line 14); rather, the changes are written to the repository, made visible to other sessions, and events are produced only when/if the transaction is committed. This would be true if we'd made several sets of transient changes interspersed with multiple Session.save() calls.
You may have noticed that EJBs no longer have to implement a remote or special interface. The only line in the above code that is at all EJB-related is the @Stateless annotation on line 1.
EJBs with bean-managed transactions
Using JCR within EJBs that manage the transactions themselves (i.e., bean-managed transactions) works the same way, except that the bean must explicitly control the transaction boundaries via the UserTransaction interface. Prior to EE6, obtaining the UserTransaction instance was not (as) standardized, though often looking it upon JNDI was the most common approach. However, with EE6, the UserTransaction can simply be injected into the bean:
ContentProvider.java
@Stateless
@TransactionManagement(BEAN)
public class ContentProvider {
@Resource
UserTransaction ut;
protected Session getSession() throws RepositoryException, NamingException {
InitialContext context = new InitialContext();
Repository repository = (Repository)context.lookup("jcr/my_repository");
return repository.login(); // uses default workspace and JAAS
}
public void uploadFile( String path, InputStream stream ) throws RepositoryException, NamingException, IOException {
Session session = getSession();
try {
ut.begin();
// Use a ModeShape utility ...
new JcrTools().uploadFile(session,path,stream);
session.save();
// Commit the transaction ...
ut.commit();
} catch ( RepositoryException e ) {
ut.rollback();
throw e;
} catch ( ... ) {
// Handle the various (and many) JTA-related transactions, rolling back as appropriate
} finally {
session.logout();
}
}
}
Explicit JTA transactions
If your application is running in an environment that supports JTA, then your application can control the boundaries of the transactions. Here's an example that uploads a file into the repository, creating also missing nodes or updating the existing nodes (the same thing as the EJB example above):
TransactionExample.java
javax.jta.TransactionManager txnMgr = ...
javax.jcr.Repository repository = ...
// Start the transaction ...
txnMgr.begin();
try {
// Obtain a session, using default workspace and credentials (which may be JAAS)
Session session = repository.login();
try {
// Use a ModeShape utility ...
new JcrTools().uploadFile(session,path,stream);
session.save();
// Commit the transaction ...
txnMgr.commit();
} finally {
session.logout();
}
} catch ( ... ) {
// Handle RepositoryException and the various (and many) JTA-related transactions, rolling back as appropriate
}
In this example, we had to code all the transaction-related logic, which is quite extensive and can be tricky to get right while properly handling all the JTA-related and repository exceptions. But once again, the transient changes made on line 11 and saved on line 12 will not actually be persisted in the repository until the transaction is committed (line 15).
This example shows all the complexity of explicit user transactions. EJBs with container-managed transactions looks really nice, doesn't it.
We could just as easily created our session outside the transaction, too:
TransactionExample.java
javax.jta.TransactionManager txnMgr = ...
javax.jcr.Repository repository = ...
Session session = repository.login();
// Start the transaction ...
txnMgr.begin();
try {
// Use a ModeShape utility ...
new JcrTools().uploadFile(session,path,stream);
session.save();
// Commit the transaction ...
txnMgr.commit();
} catch ( ... ) {
// Handle RepositoryException and the various (and many) JTA-related transactions, rolling back as appropriate
} finally {
session.logout();
}
We could have also used UserTransaction, but the details of how to obtain the UserTransaction instance varies by environment (particularly outside non-JEE6 environments).