This article launches you on a tour of Transaction Management in Quarkus applications by focusing on the standard declarative approach and the new programmatic transaction API.
Quarkus narayana extension
Quarkus, just like other Enterprise applications, uses a Transaction Manager to coordinate and expose transactions to your applications. At its core, Quarkus uses Narayana Transaction Manager, which should be familiar to you if you have experience with WildFly or JBoss Enterprise Application Platform.
Narayana is a transactions toolkit which provides support for a broad range of transaction protocol such as JTA. Narayana Transaction Manager is automatically added to your dependencies if you include Hibernate ORM in your application. As a proof of concept, check the dependency tree of an Hibernate ORM application:
mvn dependency:tree . . . . . [INFO] +- io.quarkus:quarkus-hibernate-orm:jar:2.8.0.Final:compile [INFO] | | +- org.jboss.narayana.jta:narayana-jta:jar:5.12.4.Final:compile [INFO] | | | +- org.jboss:jboss-transaction-spi:jar:7.6.0.Final:compile [INFO] | | | \- org.jboss.spec.javax.resource:jboss-connector-api_1.7_spec:jar:1.0.0.Final:compile [INFO] | | +- org.jboss.narayana.jts:narayana-jts-integration:jar:5.12.4.Final:compile
If you don’t include Hibernate as a dependency, you will have to add manually the Narayana extension to your application:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-narayana-jta</artifactId> </dependency>
A key setting of every Transactional Application is the Transaction Timeout. The default value of it is 60 seconds. However, you can change it in your configuration through the property quarkus.transaction-manager.default-transaction-timeout :
quarkus.transaction-manager.default-transaction-timeout=120
In the following example, we will show how to set the Transaction Timeout in a more granular way, on single application methods.
Building a sample Transactional Application
To demonstrate how you can manage Transactions with Quarkus we will build a simple REST application. Firstly, we will add the Model which we will persist on the Database:
@NamedQuery(name = "Ticket.findAll", query = "SELECT t FROM Ticket t") @Entity public class Ticket { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column public String name; @Column public String seat; // getter/setters omitted for brevity }
Next, let’s code the TicketService Class which persists and fetches items from the Database:
@RequestScoped public class TicketService { @Inject EntityManager em; @Transactional @TransactionConfiguration(timeout = 1000) public void buyTicket(Ticket ticket) { em.persist(ticket); System.out.println("Ticket created"); } public Ticket buyTicketProgrammaticTransaction(Ticket ticket) { QuarkusTransaction.begin(); em.persist(ticket); if (!ticket.getName().equals("scam")) { QuarkusTransaction.commit(); System.out.println("Commit!"); return ticket; } else { QuarkusTransaction.rollback(); System.out.println("Rollback!"); return null; } } public List<Ticket> listTickets() { return em.createNamedQuery("Ticket.findAll", Ticket.class) .getResultList(); } }
As you can see, the above class shows two approaches for managing Transactions:
Managing Transactions Declaratively
This is the standard way to handle Transactions in CDI applications. The @Transactional annotation provides the application the ability to declaratively control transaction boundaries on CDI Beans. You can apply it both at class and method level. Method level annotations override those at the class level. To learn more about CDI and Transactions check this article: Using Transactions in CDI Beans
Next, please note we have included the @io.quarkus.narayana.jta.runtime.TransactionConfiguration annotation to specify the Transaction Timeout for the method buyTicket.
Finally, to specify how to Transaction Manager manages a new Transaction you can specify an attribute in the @Transactional annotation. The default value is REQUIRED which starts a transaction if none was active. It uses the existing one otherwise.
@Transactional(SUPPORTS)
Managing Transactions Programmatically
The method buyTicketProgrammaticTransaction shows how to use the new io.quarkus.narayana.jta.QuarkusTransaction API to handle transactions programmatically.
Please note, this feature requires Quarkus 2.8.0 or newer.
The io.quarkus.narayana.jta.QuarkusTransaction can use a set of static methods to demarcate the boundaries of transactions:
- QuarkusTransaction.begin(): demarcates the beginning of a JTA Transaction.
- QuarkusTransaction.commit(): commits a Transaction
- QuarkusTransaction.rollback(): marks the Transaction for Rollback
In our method buyTicketProgrammaticTransaction we are using the content of a field name to determine if the Transaction should commit or rollback.
Exposing the Transaction Service
Next, to complete our example, we will add a REST Endpoint which maps the two methods to buy a Ticket and one to fetch the existing Tickets:
@Path("/tickets") public class TicketResource { @Inject TicketService service; @POST @Path("/buy1") public Response buyTicket1(Ticket ticket) { service.buyTicket(ticket); return Response.ok(ticket).status(201).build(); } @POST @Path("/buy2") public Response buyTicket2(Ticket ticket) { ticket = service.buyTicketProgrammaticTransaction(ticket); System.out.println(ticket); if (ticket == null) { throw new WebApplicationException("Ticket invalid", 500); } return Response.ok(ticket).status(201).build(); } @GET public List<Ticket> ListTickets() { return service.listTickets(); } }
Finally, with the application up and running, you can start testing it:
curl -X POST http://localhost:8080/tickets/buy1 -H 'Content-Type: application/json' -d '{"name":"john","seat":"abc"}' curl -X POST http://localhost:8080/tickets/buy2 -H 'Content-Type: application/json' -d '{"name":"frank","seat":"def"}' curl -X POST http://localhost:8080/tickets/buy2 -H 'Content-Type: application/json' -d '{"name":"scam","seat":"def"}'
As you can see from the Console, the last POST will cause the TransactionManager to rollback the Transaction:
Handling Transaction Exceptions
You might have noticed that our TicketService Class does not use any try/catch semantic to handle the Transaction outcome (commit/rollback). As a matter of fact, when using the io.quarkus.narayana.jta.QuarkusTransaction API, it is up to you to throw or handle Exceptions in response to Transaction outcomes if your application needs it.
Also, keep in mind that all transactions are bound to the CDI Context. Therefore, all active transactions will be automatically be rolled back when the CDI Context is destroyed. For example with @RequestScoped Beans when the Request is over.
Within the Product documentation, you can check how to use Lambda Scoped Transactions to handle exceptions and transaction semantics using Lambda Expressions and io.quarkus.narayana.jta.QuarkusTransaction API,.
Finally, If you prefer the legacy approach, you can still inject the UserTransaction object in your Quarkus applications:
@Inject UserTransaction transaction; public Ticket buyTicketLegacyTransaction(Ticket ticket) throws Exception { try { transaction.begin(); em.persist(ticket); transaction.commit(); } catch(Exception e) { // do something on Tx failure transaction.rollback(); } return ticket;
As you can see, when using the legacy UserTransaction object you have to handle or throw any Exception thrown by the UserTransaction API.
Source code
The source code for the example application is available here: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/transactions