Транзакции XA полезны и из коробки, Play 2.0 сегодня не имеет их поддержки. Здесь я покажу, как добавить эту поддержку:
Прежде всего, несколько примеров, когда XA полезен:
— JPA использует два физических соединения, если вы используете сущности из двух разных persistence.xml — эти два соединения, возможно, должны быть зафиксированы в одной транзакции, поэтому XA — ваш единственный вариант
— Подтверждение изменения в база данных и одновременно отправка сообщения в JMS. Например, вы хотите гарантировать, что электронное письмо будет отправлено после того, как вы успешно отправите заказ в базу данных, асинхронно. Есть и другие способы, но JMS предоставляет транзакционный способ сделать это с небольшими накладными расходами на необходимость думать о сбое.
— Запись в физически другую базу данных по любой из нескольких политических причин (устаревшая система, другой отдел, отвечающий за другой сервер базы данных / разные бюджеты).
— Увидетьhttp://docs.codehaus.org/display/BTM/FAQ#FAQ-WhywouldIneedatransactionmanager
Так что, на мой взгляд, XA — это то, что Play нуждается в «поддержке».
Добавить поддержку очень просто. Я создал игровой плагин, который основан на Bitronix. Ресурсы конфигурируются в дереве JNDI Bitronix (почему Play использует файл конфигурации, а не JNDI ?! в любом случае …) Вы запускаете транзакцию так: «withXaTransaction»:
def someControllerMethod = Action {
withXaTransaction { ctx =>
TicketRepository.
addValidation(user.get, bookingRef, ctx)
ValidationRepository.addValidation(bookingRef, user.get, ctx)
}
val tickets = TicketRepository.getByEventUid(eventUid)
Ok(views.html.ticketsInEvent(eventUid, getTickets(eventUid), user, eventValidationForm))
}
Объект ctx — это XAContext (мой собственный класс), который позволяет вам искать ресурсы как источник данных или устанавливать откат в случае сбоя. Таким образом, проверочное хранилище делает это, используя ScalaQuery (я использовал «withSession», а не «withTransaction!»):
def addValidation(bookingRef: String, validator: User, ctx: XAContext) = {
val ds = ctx.lookupDS("jdbc/maxant/scalabook_admin")
Database.forDataSource(ds) withSession { implicit db: Session =>
Validations.insert(Validation(bookingRef, validator.email, new java.sql.Timestamp(now)))
}
}
А репозиторий делает с JMS следующее:
def addValidation(user: User, bookingRef: String, ctx: XAContext) = {
val xml =
<ticketValidation>
<bookingReference>{bookingRef}</bookingReference>
<validatorId>{user.email}</validatorId>
</ticketValidation>
val qcf = ctx.lookupCF("jms/maxant/scalabook/ticketvalidations")
val qc = qcf.createConnection("ticketValidation","password")
val qs = qc.createSession(false, Session.AUTO_ACKNOWLEDGE)
val q = qs.createQueue("ticketValidationQueue") //val q = ctx.lookup(QUEUE).asInstanceOf[Queue]
val sender = qs.createProducer(q)
val m = qs.createTextMessage(xml.toString)
sender.send(m)
sender.close
qs.close
qc.close
}
Я протестировал его с записью в MySQL и отправкой JMS-сообщения в JBoss (HornetQ), и, похоже, он работает хорошо (за исключением того, что заставить hornetQ играть с Bitronix было сукой — см. Здесь:
https://community.jboss.org/ резьба / 206180? tstart = 0 ).
Scala-код для поддержки XA:
package ch.maxant.scalabook.play20.plugins.xasupport
import play.api.mvc.RequestHeader
import play.api.mvc.Results
import play.api.mvc.Request
import play.api.mvc.AnyContent
import play.api.mvc.Result
import play.api.mvc.Action
import play.api.mvc.Security
import play.api._
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import ch.maxant.scalabook.persistence.UserRepository
import bitronix.tm.TransactionManagerServices
import java.util.Hashtable
import javax.naming.Context._
import javax.naming.InitialContext
import javax.sql.DataSource
import bitronix.tm.BitronixTransaction
import java.io.File
import org.scalaquery.session.Database
import org.scalaquery.SQueryException
import scala.collection.mutable.ListBuffer
import java.sql.Connection
import java.sql.SQLException
import org.scalaquery.session.Session
import bitronix.tm.BitronixTransactionManager
import javax.jms.ConnectionFactory
class XAContext {
private val env = new Hashtable[String, String]()
env.put(INITIAL_CONTEXT_FACTORY, "bitronix.tm.jndi.BitronixInitialContextFactory")
private val namingCtx = new InitialContext(env);
var rollbackOnly = false
def lookup(name: String) = {
namingCtx.lookup(name)
}
def lookupDS(name: String) = {
lookup(name).asInstanceOf[DataSource]
}
def lookupCF(name: String) = {
lookup(name).asInstanceOf[ConnectionFactory]
}
}
trait XASupport { self: Controller =>
private lazy val tm = play.api.Play.current.plugin[XASupportPlugin] match {
case Some(plugin) => plugin.tm
case None => throw new Exception("There is no XASupport plugin registered. Make sure it is enabled. See play documentation. (Hint: add it to play.plugins)")
}
/**
* Use this flow control to make resources used inside `f` commit with the XA protocol.
* Conditions: get resources like drivers or connection factories out of the context passed to f.
* Connections are opened and closed as normal, for example by the withSession flow control offered
* by ScalaQuery / SLICK.
*/
def withXaTransaction[T](f: XAContext => T): T = {
tm.begin
//get a ref to the transaction, in case when we want to commit we are no longer on the same thread and TLS has lost the TX.
//we have no idea what happens inside f! they might spawn new threads or send work to akka asyncly
val t = tm.getCurrentTransaction
Logger("XASupport").info("Started XA transaction " + t.getGtrid())
val ctx = new XAContext()
var completed = false
try{
val result = f(ctx)
completed = true
if(!ctx.rollbackOnly){
Logger("XASupport").info("committing " + t.getGtrid() + "...")
t.commit
Logger("XASupport").info("committed " + t.getGtrid())
}
result
}finally{
if(!completed || ctx.rollbackOnly){
//in case of exception, or in case of set rollbackOnly = true
Logger("XASupport").warn("rolling back (completed=" + completed + "/ctx.rollbackOnly=" + ctx.rollbackOnly)
t.rollback
}
}
}
}
class XASupportPlugin(app: play.Application) extends Plugin {
protected[plugins] var tm: BitronixTransactionManager = null
override def onStart {
//TODO how about getting config out of jar!
val file = new File(".", "app/bitronix-default-config.properties").getAbsolutePath
Logger("XASupport").info("Using Bitronix config at " + file)
val prop = System.getProperty("bitronix.tm.configuration", file) //default
System.setProperty("bitronix.tm.configuration", prop) //override with default, if not set
//start the TM
tm = TransactionManagerServices.getTransactionManager
Logger("XASupport").info("Started TM with resource config " + TransactionManagerServices.getConfiguration.getResourceConfigurationFilename)
}
override def onStop {
//on graceful shutdown, we want to shutdown the TM too
Logger("XASupport").info("Shutting down TM")
tm.shutdown
Logger("XASupport").info("TM shut down")
}
}
Используйте код по своему усмотрению, я раздаю его бесплатно ? Только не жалуйтесь, если он не работает ?
Было бы здорово увидеть, что этот плагин расширен и превращен во что-то чуть более готовое к работе , Еще приятнее было бы, чтобы Play изначально поддерживал диспетчер транзакций, включая извлечение ресурсов из JNDI.
Повеселись!