Транзакции 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.
Повеселись!