Chapter 10. Distributed Transactions (XA)
10.1. Introduction
Immutant encapsulates the distributed transaction support provided by JBoss AS7 within the immutant.xa namespace.
A distributed transaction is one in which multiple types of resources may participate. The most common example of a transactional resource is a relational database, but in Immutant both caching (backed by Infinispan) and messaging (backed by HornetQ) are also automatically transactional. Technically speaking, they provide implementations of the XA protocol, and any back-end data store that does so may participate in an Immutant transaction.
This allows your application to say, tie the success of a SQL database update or the storage of an entry on a replicated data grid to the delivery of a message to a remote queue, i.e. the message is only sent if the database and data grid update successfully. If any single component of an XA transaction fails, all of them rollback.
The immutant.xa
namespace contains only two functions:
datasource | Creates a valid XA DataSource for the most common SQL databases |
transaction | A macro that wraps its body in a transaction unless one is already active |
More fine-grained transactional control is available through the immutant.xa.transaction namespace, described below.
10.2. Creating XA DataSources
In order for your database to participate in an XA transaction, an XA DataSource must be created for it. In Immutant, you have two options: either 1) call immutant.xa/datasource from your application, or 2) use the JBoss AS7 configuration facilities. The former is simple and recommended for most deployments, but the latter can be easier to manage when multiple applications share the same DataSource in a clustered environment.
10.2.1. For use with clojure.java.jdbc
The spec you pass to clojure.java.jdbc/with-connection depends on
how you create the DataSource. If you call
immmutant.xa/datasource
, then associate the :datasource
key to its
result.
Otherwise, set the :name
key to the JNDI name you set for the
xa-datasource
in the JBoss AS7 configuration.
And any library based on clojure.java.jdbc should work just fine with that spec.
10.2.1.1. Using immutant.xa/datasource
To create your own DataSource, you must make the appropriate JDBC
driver available. This is easily done with a Leiningen project in
project.clj
. The following drivers have been tested
successfully:
[com.h2database/h2 "1.3.160"] ; H2 [org.clojars.gukjoon/ojdbc "1.4"] ; Oracle [mysql/mysql-connector-java "5.1.22"] ; MySQL [postgresql "9.0-801.jdbc4"] ; Postgres [net.sourceforge.jtds/jtds "1.2.4"] ; MS SQL Server
Here's an example creating a DataSource for an in-memory H2 database:
(defonce ds (immutant.xa/datasource "foo" {:adapter "h2" :database "mem:foo"})) (jdbc/with-connection {:datasource ds} (jdbc/create-table :things [:name "varchar(50)"]))
Here's an example creating an Oracle DataSource for an Amazon RDS instance:
;;; rds-create-db-instance myinstance -s 10 -c db.m1.small -e oracle-se -u myuser -p mypassword --db-name mydb (defonce ds (ixa/datasource "foo" {:adapter "oracle" :host "myinstance.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com" :username "myuser" :password "mypassword" :database "mydb"})) (jdbc/with-connection {:datasource ds} ...)
10.2.1.2. Using an AS7-configured DataSource
Once you've configured your XA DataSource in AS7, you simply refer to its JNDI name:
(jdbc/with-connection {:name "java:jboss/datasources/ExampleXADS"} (jdbc/create-table :things [:name "varchar(50)"]))
10.3. Defining Transactions
A transaction is easily defined using the immutant.xa/transaction macro:
(ns xa.example (:require [immutant.xa :as xa] [immutant.cache :as cache] [immutant.messaging :as msg] [clojure.java.jdbc :as sql])) (xa/transaction (sql/with-connection {:datasource my-ds} (sql/insert-records :things {:name "foo"})) (cache/put my-cache :a 1) (msg/publish "/queue/test" "success!"))
In the example above, we insert a record into a SQL database, write
an entry to a cache and publish a message to a queue, all within the
same XA transaction. If the body of xa/transaction
runs without
tossing an exception, the transaction will be committed. Otherwise
it will be rolled back. All or nothing. Simple.
10.3.1. Transaction Scope
When transactional components interact, the state of a transaction
when a particular function is invoked isn't always easy to predict.
For example, can a function that requires a transaction assume one
has been started prior to its invocation? In JEE container-managed
persistence, a developer answers these questions using the
@TransactionAttribute
annotation.
But annotations are gross, right? :)
So instead, the JEE transaction attributes are represented as
Clojure macros. In fact, the xa/transaction
macro shown above is
merely an alias for immutant.xa.transaction/required, which is the
implicit attribute used in JEE. There are a total of 6 macros:
required | Execute within current transaction, if any, otherwise start a new one, execute, commit or rollback. |
requires-new | Suspend current transaction, if any, start a new one, execute, commit or rollback, and resume the suspended one. |
not-supported | Suspend current transaction, if any, and execute without a transaction. |
supports | Execute the body whether there's a transaction or not; may lead to unpredictable results |
mandatory | Toss an exception if there's no active transaction |
never | Toss an exception if there is an active transaction |
These macros give the developer complete declarative control over the transactional semantics of their application as its functional chunks are combined.