Прежде всего, несколько примеров, когда XA полезен:
— JPA использует два физических соединения, если вы используете сущности из двух разных persistence.xml — эти два соединения, возможно, должны быть зафиксированы в одной транзакции, поэтому XA — ваш единственный вариант
— фиксация изменений в базе данных и одновременная фиксация сообщения в JMS. Например, вы хотите гарантировать, что электронное письмо будет отправлено после того, как вы успешно отправите заказ в базу данных, асинхронно. Есть и другие способы, но JMS предоставляет транзакционный способ сделать это с небольшими накладными расходами на необходимость думать о сбое.
— Запись в физически другую базу данных по любой из нескольких политических причин (устаревшая система, другой отдел, отвечающий за другой сервер базы данных / разные бюджеты).
— См. Http://docs.codehaus.org/display/BTM/FAQ#FAQ-WhywouldIneedatransactionmanager.
Так что, как я понимаю, XA — это то, что Play должен «поддерживать».
Добавить поддержку очень просто. Я создал игровой плагин, который основан на Bitronix. Ресурсы настраиваются в дереве JNDI Bitronix (в любом случае Play использует файл конфигурации, а не JNDI ?! в любом случае…). Вы запускаете транзакцию следующим образом: «withXaTransaction»:
01
02
03
04
05
06
07
08
09
10
|
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!»):
1
2
3
4
5
6
|
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 следующее:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
def addValidation(user : User, bookingRef : String, ctx : XAContext) = { val xml = {bookingRef} {user.email} 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:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
package ch.maxant.scalabook.play 20 .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.
Приятного кодирования и не забудьте поделиться!
Ссылка: Play 2.0 Framework и транзакции XA от нашего партнера JCG Антона Кучера в блоге The Kitchen in the Zoo .