Mar 24, 2009

Spring: Distributed Transactions across JCR and MySQL Database

Before following the steps described next, please be aware that this isn't an example of something you should use in production, but merely a sketch of how to wire Jackrabbit repository and a MySQL database to participate in distributed transactions. I haven't worked with XA before, nor I have a lot of experience with Spring, and it took me some time to get this working (mostly because of my ignorance in this area), so certain things are simplified, certain libraries are outdated, and sometimes I am not entirely sure what exactly I am doing. Hopefully, in future I will improve and update this example.

If you're new to Spring and transactions, you might want to read Java Transaction Design Strategies, a free book from InfoQ, and Chapter 9 of the Spring Reference.

To have transactions distributed across multiple datasources in Spring, you need an external transaction manager. By now, I am aware of three distributions that provide such functionality. Those are Atomikos, JOTM and Jencks. I will be using Jencks despite the fact that the library development seems to be discontinued since 2007, and it's not supported in any way. The reason is simply that it's the only library I've managed to configure by now. The even-more-outdated Jackrabbit-JTA example uses Jencks as well.

To deal with the problem of configuration, I've created a simple web application called “Wijax” (as I'm using Wicket for presentation, so it's Wicket-Jackrabbit-Spring). You can get the source of it in a form of a NetBeans project here.

Once you have your minimal Wicket-Spring application up and running, it's time to add some JCR.

Dependencies



If you haven't yet discovered the magic and pain of using Maven (like yours truly here), you will need the following libraries:


Jackrabbit-related JARs




  • Concurrent

  • Commons-Collections

  • Commons IO

  • Derby

  • Jackrabbit-API

  • Jackrabbit-JCA

  • Jackrabbit-Core

  • Jackrrabbit-JCR-Commons

  • Jackrrabbit-Text-Extractors

  • Jackrrabbit-SPI

  • Jackrrabbit-SPI-Commons

  • Lucene Core


All Jackrabbit-related jars might be found in Jackrabbit-Webapp distribution.

Jencks





For the latter two links I must thank Les Hazlewood who pointed those out at the Jencks discussion forum, potentially saving me a couple of hours of searching. This is not all, however. Jencks depends on JTA and JCA packages, and they are included in Jencks-All package. However, in the version of Jencks 2.1 they are both outdated as versions 1.0 of both libraries are included, when they are supposed to be 1.1 and 1.5 respectively. I just replaced whatever was found in javax.* folders included in Jencks-All by required versions.

Spring



You also might want to get AspectJWeaver in case you will be using AOP-based Spring configuration (like I'm going to) and move Commons Logging from Tomcat lib folder to WEB-INF in case you have it there and experiencing logging problems.

Obviously, all the libraries mentioned above can be found in the Wijax project archive, but I provided links just so you might want to get newer versions of maintaned jars.

Business Objects


We'll have two business objects called “Dumb” and “Silly” with Dumb being stored in database and Silly kept in repository. Both will have an “id” and “version” parameters, however, we will only deal with the version.

Create MySQL database “wijax” and import this sql in it:

CREATE TABLE IF NOT EXISTS `dumb` (
`id` bigint(11) unsigned NOT NULL auto_increment,
`version` int(5) unsigned NOT NULL default '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ;


For both objects, we'll have DAOs and DTOs.

Dumb DTO:


package ru.sadninja.wijax.dumb;

import java.io.Serializable;

/**
*
* @author sp
*/
public class DumbDto implements Serializable {

private final long serialVersionUID = 1L;

private long id;
private int version;

public DumbDto(long id) {
this.id = id;
}

/**
* @return the id
*/
public long getId() {
return id;
}

/**
* @return the version
*/
public int getVersion() {
return version;
}

/**
* @param version the version to set
*/
public void setVersion(int version) {
this.version = version;
}
}


Dumb DAO interface:


package ru.sadninja.wijax.dumb;

import java.io.Serializable;

/**
*
* @author sp
*/
public interface DumbDao extends Serializable {

public void save(DumbDto dto);

public void saveWithRollback(DumbDto dto);

public DumbDto load(long id);

}


Dumb DAO JDBC implementation:


package ru.sadninja.wijax.dumb;

import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.log4j.Logger;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

/**
*
* @author sp
*/
public class DumbJdbcDao extends JdbcDaoSupport implements DumbDao {

private final static long serialVersionUID = 1L;

static Logger dLogger = Logger.getLogger(DumbJdbcDao.class.getName());

private final static String INSERT_DUMB_QUERY_STRING =
"insert into dumb (version) values (?)";

private final static String SAVE_DUMB_QUERY_STRING =
"update dumb set version=? where id=?";

private final static String LOAD_DUMB_BY_ID =
"select version from dumb where id=?";

@Override
public void save(DumbDto dumb) {
if (dumb == null || dumb.getId() == 0) {
insert(dumb);
} else {
Object[] param = new Object[] { dumb.getVersion(), dumb.getId() };
if (getJdbcTemplate().update(SAVE_DUMB_QUERY_STRING, param) == 0) {
insert(dumb);
}
}

}

@Override
public void saveWithRollback(DumbDto dumb) {
throw new RuntimeException("Whoops, runtime exception");
}

protected void insert(DumbDto dumb) {
Object[] param = new Object[] { dumb.getVersion() };
getJdbcTemplate().update(INSERT_DUMB_QUERY_STRING, param);
}

@Override
public DumbDto load(final long id) {
dLogger.info("reading dumb #" + id);
Object[] params = new Object[] { id };
DumbDto dumb = (DumbDto) getJdbcTemplate().query(LOAD_DUMB_BY_ID, params,
new ResultSetExtractor() {
public Object extractData(ResultSet rs)
throws SQLException, DataAccessException {
rs.next();
DumbDto dumb = new DumbDto(id);
dumb.setVersion(rs.getInt("version"));
return dumb;
}

});

dLogger.info("dumb #" + id + " is of version " + dumb.getVersion());

return dumb;
}
}


Silly DTO:


package ru.sadninja.wijax.silly;

import java.io.Serializable;

/**
*
* @author sp
*/
public class SillyDto implements Serializable {

private final long serialVersionUID = 1L;

private long id;
private int version;

public SillyDto(long id) {
this.id = id;
}

/**
* @return the id
*/
public long getId() {
return id;
}

/**
* @return the version
*/
public int getVersion() {
return version;
}

/**
* @param version the version to set
*/
public void setVersion(int version) {
this.version = version;
}

}


Silly DAO:


package ru.sadninja.wijax.silly;

import java.io.Serializable;

/**
*
* @author sp
*/
public interface SillyDao extends Serializable {

public void read();

public void save(SillyDto silly);

public void saveWithRollback(SillyDto silly);

}


Silly DAO JCR implementation:


/**
*
* @author sp
*/
public class SillyJcrDao extends JcrDaoSupport implements SillyDao {

private final static long serialVersionUID = 1L;
static Logger sLogger = Logger.getLogger(SillyJcrDao.class.getName());

@Override
public void read() {
sLogger.debug("reading...");
final Session session = getSession();
try {
Node root = session.getRootNode();
if (root.hasNode("silly")) {
sLogger.info("silly node present");
Node silly = root.getNode("silly");
if (silly.hasProperty("version")) {
sLogger.info("silly is of version "
+ silly.getProperty("version").getLong());
}
} else {
sLogger.info("silly node missing");
}
} catch (RepositoryException re) {
throw convertJcrAccessException(re);
}
}

@Override
public void save(SillyDto sillyDto) {
sLogger.debug("saving...");
final Session session = getSession();
try {
Node root = session.getRootNode();

Node silly;
if (root.hasNode("silly")) {
silly = root.getNode("silly");
} else {
sLogger.debug("silly node missing, create one");
silly = root.addNode("silly");
}
silly.setProperty("version", sillyDto.getVersion());
session.save();
sLogger.info("silly is now of version "
+ silly.getProperty("version").getLong());
} catch (RepositoryException re) {
throw convertJcrAccessException(re);
}
}

@Override
public void saveWithRollback(SillyDto sillyDto) {
throw new RuntimeException("Whoa, runtime exception!");
}

}


Now it gets interesting: a service which performs a transactions with both DAOs involved. I called it Creepy Service.

An interface:



package ru.sadninja.wijax.service;

import java.io.Serializable;

/**
*
* @author sp
*/
public interface CreepyService extends Serializable {

public void updateVersion(int n);

public void readVersion();

}



And the implementation:


package ru.sadninja.wijax.service;

import ru.sadninja.wijax.dumb.DumbDao;
import ru.sadninja.wijax.dumb.DumbDto;
import ru.sadninja.wijax.silly.SillyDao;
import ru.sadninja.wijax.silly.SillyDto;

/**
*
* @author sp
*/
public class CreepyServiceImpl implements CreepyService {

private final static long serialVersionUID = 2L;

private DumbDao dumbDao;
private SillyDao sillyDao;

@Override
public void updateVersion(int n) {

DumbDto dumb = new DumbDto(1);
dumb.setVersion(n);
dumbDao.save(dumb);

SillyDto silly = new SillyDto(1);
silly.setVersion(n);
sillyDao.save(silly);

if (n == 11) {
sillyDao.saveWithRollback(silly);
}

// dumbDao.saveWithRollback(dumb);
}

@Override
public void readVersion() {
dumbDao.load(1);
sillyDao.read();
}

public void setDumbDao(DumbDao dumbDao) {
this.dumbDao = dumbDao;
}

public DumbDao getDumbDao() {
return dumbDao;
}

/**
* @return the sillyDao
*/
public SillyDao getSillyDao() {
return sillyDao;
}

/**
* @param sillyDao the sillyDao to set
*/
public void setSillyDao(SillyDao sillyDao) {
this.sillyDao = sillyDao;
}
}


And a page we will call be calling all this from:


package ru.sadninja.wijax.web.page;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.springframework.dao.DataAccessException;
import ru.sadninja.wijax.service.CreepyService;
import ru.sadninja.wijax.silly.SillyDao;

/**
*
* @author sp
*/
public class SillyPage extends WebPage {

private final static long serialVersionUID = 2L;

@SpringBean(name="sillyDao")
SillyDao sillyDao;
@SpringBean(name="creepyService")
CreepyService creepyService;

private int version = 0;

public SillyPage() {
sillyDao.read();
add(new SillyForm());
}

public int getVersion() {
return version;
}

public void setVersion(int version) {
this.version = version;
}

class SillyForm extends Form {
public SillyForm() {
super("sillyForm", new CompoundPropertyModel(SillyPage.this));
add(new FeedbackPanel("feedback"));
add(new TextField("version"));
}

@Override
public void onSubmit() {
try {
creepyService.updateVersion(getVersion());
setResponsePage(SillyPage.class);
} catch (DataAccessException dae) {
error(dae.getMessage());
} catch (RuntimeException re) {
error("Got a transaction exception: " + re.getMessage());
}
}
}
}


HTML for the page:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<form wicket:id="sillyForm">
<div wicket:id="feedback"></div>
<div>
<input type="text" wicket:id="version" />
<input type="submit" value="write" />
</div>
</form>
</body>
</html>


Okay, here comes the sweetest part: Spring application context configuration. What we have to do is provide some sort of access to both data sources, make the accessing mechanisms aware of the global transaction context and configure our DAOs and Service to be transactional.

Here goes:

/WEB-INF/applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">


<bean id="wijaxApplication" class="ru.sadninja.wijax.WijaxApplication" />

<bean id="jtaTransactionManager" class="org.jencks.factory.TransactionManagerFactoryBean"/>

<bean id="jrConnectionManager"
class="org.jencks.factory.ConnectionManagerFactoryBean">
<property name="transactionManager">
<ref local="jtaTransactionManager"/>
</property>
<property name="transaction" value="xa"/>
</bean>

<bean id="jdbcConnectionManager"
class="org.jencks.factory.ConnectionManagerFactoryBean">
<property name="transactionManager">
<ref local="jtaTransactionManager"/>
</property>
<property name="transaction" value="xa"/>
</bean>

<bean id="jdbcManagedConnectionFactory" class="org.jencks.tranql.DataSourceMCF">
<property name="driverName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/wijax"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
</bean>

<bean id="jdbcXaDataSource" class="org.jencks.factory.ConnectionFactoryFactoryBean">
<property name="managedConnectionFactory" ref="jdbcManagedConnectionFactory"/>
<property name="connectionManager" ref="jdbcConnectionManager"/>
</bean>

<bean id="repository" class="org.jencks.factory.ConnectionFactoryFactoryBean">
<property name="managedConnectionFactory"
ref="repositoryManagedConnectionFactory"/>
<property name="connectionManager" ref="jrConnectionManager"/>
</bean>

<bean id="repositoryManagedConnectionFactory"
class="org.apache.jackrabbit.jca.JCAManagedConnectionFactory">
<property name="homeDir" value="/var/jackrabbit2"/>
<property name="configFile" value="/var/jackrabbit2/repository.xml" />
</bean>

<bean id="jcrSessionFactory" class="org.springmodules.jcr.JcrSessionFactory">
<property name="repository" ref="repository"/>
<property name="credentials">
<bean class="javax.jcr.SimpleCredentials">
<constructor-arg index="0" value=""/>
<constructor-arg index="1" value="">
</constructor-arg>
</bean>
</property>
</bean>

<!-- The JCR Template. -->
<bean id="jcrTemplate" class="org.springmodules.jcr.JcrTemplate">
<property name="sessionFactory" ref="jcrSessionFactory"/>
<property name="allowCreate" value="true"/>
</bean>

<tx:advice id="txSillyAdvice" transaction-manager="jtaTransactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true" />
<tx:method name="*" />
</tx:attributes>
</tx:advice>

<aop:config>
<aop:pointcut id="sillyDaoOperation"
expression="execution(* ru.sadninja.wijax.silly.SillyDao.*(..))" />
<aop:advisor advice-ref="txSillyAdvice" pointcut-ref="sillyDaoOperation" />
</aop:config>

<bean id="sillyDao" class="ru.sadninja.wijax.silly.SillyJcrDao">
<property name="template" ref="jcrTemplate"/>
</bean>

<tx:advice id="txDumbAdvice" transaction-manager="jtaTransactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true" />
<tx:method name="*" />
</tx:attributes>
</tx:advice>

<aop:config>
<aop:pointcut id="dumbDaoOperation"
expression="execution(* ru.sadninja.wijax.dumb.DumbDao.*(..))" />
<aop:advisor advice-ref="txDumbAdvice" pointcut-ref="dumbDaoOperation" />
</aop:config>

<bean id="dumbDao" class="ru.sadninja.wijax.dumb.DumbJdbcDao">
<property name="dataSource" ref="jdbcXaDataSource"/>
</bean>

<tx:advice id="txCreepyAdvice" transaction-manager="jtaTransactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true" />
<tx:method name="*" />
</tx:attributes>
</tx:advice>

<aop:config>
<aop:pointcut id="creepyServiceOperation"
expression="execution(* ru.sadninja.wijax.service.CreepyService.*(..))" />
<aop:advisor advice-ref="txCreepyAdvice" pointcut-ref="creepyServiceOperation" />
</aop:config>

<bean id="creepyService" class="ru.sadninja.wijax.service.CreepyServiceImpl">
<property name="dumbDao" ref="dumbDao" />
<property name="sillyDao" ref="sillyDao" />
</bean>

</beans>


My two main sources of inspiration for the above config were excerpts from the Chapter Nine mentioned above, and an example of Jencks configuration for multiple datasources.

Finally, you might want to add log4j.xml to watch Spring driving the transactions.

Test-drive



That seems like it. If you've done everything right, accessing SillyPage for the first time will give the output message that silly node is missing. Try putting something except “11” (as that will throw an exception) in the form and submitting it. In the log, you should get something like:


234824 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Creating new transaction with name [ru.sadninja.wijax.service.CreepyService.updateVersion]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
234825 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Participating in existing transaction
234825 [http-8084-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL update
234825 [http-8084-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL statement [update dumb set version=? where id=?]
234825 [http-8084-1] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
234828 [http-8084-1] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Registering transaction synchronization for JDBC Connection
234830 [http-8084-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - SQL update affected 1 rows
234831 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Participating in existing transaction
234831 [http-8084-1] DEBUG ru.sadninja.wijax.silly.SillyJcrDao - saving...
234831 [http-8084-1] DEBUG ru.sadninja.wijax.silly.SillyJcrDao - silly node missing, create one
234832 [http-8084-1] INFO ru.sadninja.wijax.silly.SillyJcrDao - silly is now of version 4
234832 [http-8084-1] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource
234832 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Initiating transaction commit
234979 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Creating new transaction with name [ru.sadninja.wijax.silly.SillyDao.read]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
234979 [http-8084-1] DEBUG ru.sadninja.wijax.silly.SillyJcrDao - reading...
234980 [http-8084-1] INFO ru.sadninja.wijax.silly.SillyJcrDao - silly node present
234980 [http-8084-1] INFO ru.sadninja.wijax.silly.SillyJcrDao - silly is of version 4
234980 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Initiating transaction commit


If you check your “dumb” table, you'll see a row with id=1 and version=4.

Now try and put “11” in the form on the SillyPage. You should get an error message on the page, and in the output console:


407268 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Creating new transaction with name [ru.sadninja.wijax.service.CreepyService.updateVersion]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
407268 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Participating in existing transaction
407268 [http-8084-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL update
407268 [http-8084-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL statement [update dumb set version=? where id=?]
407268 [http-8084-1] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
407270 [http-8084-1] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Registering transaction synchronization for JDBC Connection
407271 [http-8084-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - SQL update affected 1 rows
407272 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Participating in existing transaction
407272 [http-8084-1] DEBUG ru.sadninja.wijax.silly.SillyJcrDao - saving...
407273 [http-8084-1] INFO ru.sadninja.wijax.silly.SillyJcrDao - silly is now of version 11
407274 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Participating in existing transaction
407274 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Participating transaction failed - marking existing transaction as rollback-only
407274 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Setting JTA transaction rollback-only
407274 [http-8084-1] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource
407274 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Initiating transaction rollback


Now, try to hit “Back” and reload the page. You'll see that the silly version in repository is still “4”:


539452 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Creating new transaction with name [ru.sadninja.wijax.silly.SillyDao.read]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
539452 [http-8084-1] DEBUG ru.sadninja.wijax.silly.SillyJcrDao - reading...
539453 [http-8084-1] INFO ru.sadninja.wijax.silly.SillyJcrDao - silly node present
539453 [http-8084-1] INFO ru.sadninja.wijax.silly.SillyJcrDao - silly is of version 4
539453 [http-8084-1] DEBUG org.springframework.transaction.jta.JtaTransactionManager - Initiating transaction commit

1 comment:

blognya pancara... said...

it's work... :) :)

thanks..