Brian's Waste of Time

Wed, 11 Apr 2007

Guice with Spring Transactions

I futzed a bit with setting up Spring transaction handling in Guice. It was shockingly straightfirward :-)

So, there are several ways to do it, but I did it via having a Module export both a DBI and apply the transaction interceptor. Frankly, I probably wouldn't use this class in a real project because I would just wire it up more specific to how I needed it. However, for the common case of one data source, and annotated transactions. Bingo!

    package org.skife.jdbi.v2.unstable.guice;

    import com.google.inject.Module;
    import com.google.inject.Binder;
    import com.google.inject.matcher.Matchers;

    import javax.sql.DataSource;

    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.interceptor.TransactionInterceptor;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.skife.jdbi.v2.spring.DBIFactoryBean;
    import org.skife.jdbi.v2.IDBI;

    public class SpringTransactionalGuiceModule implements Module
    {
        private final DataSource ds;

        public SpringTransactionalGuiceModule(final DataSource ds)
        {
            this.ds = ds;
        }

        public void configure(Binder binder)
        {
            final PlatformTransactionManager ptm = new DataSourceTransactionManager(this.ds);
            final DBIFactoryBean bean = new DBIFactoryBean();
            bean.setDataSource(ds);
            try {
                final IDBI dbi = (IDBI) bean.getObject();
                binder.bind(IDBI.class).toInstance(dbi);
            }
            catch (Exception e) {
                binder.addError(e);
                return;
            }
            binder.bindInterceptor(Matchers.any(), Matchers.annotatedWith(Transactional.class),
                                   new TransactionInterceptor(ptm, new AnnotationTransactionAttributeSource()));
        }
    }

This module reuses the jDBI Spring transaction integration glue magic stuff, hence instantiating the DBI instance via the DBIFactoryBean. It then binds Spring's TransactionInterceptor, telling it to base its tx magic on annotations via the AnnotationTransactionAttributeSource and the PlatformTransactionManager.

This could bind the platform transaction manager as well, but for now I don't need it, so I didn't.

To exercise it, I need another module which has my actual things in it, and I need to do something, like:

    public static class ThingModule implements Module
    {
        public void configure(Binder binder)
        {
            binder.bind(Thing.class);
        }
    }

    public static class Thing
    {
        private final IDBI dbi;

        @Inject
        Thing(IDBI dbi)
        {
            this.dbi = dbi;
        }

        @Transactional
        public void excute(final Callback cb)
        {
            final Handle handle = DBIUtil.getHandle(dbi);
            cb.call(handle);
        }
    }

    public interface Callback
    {
        public void call(Handle handle);
    }

The only transactional element is the Thing#excute method, and it is using the Spring 2.0 @Transactional annotation.

I like to use callbacks for testing transactional stuff like this, reduces the clutter. The only really nasty bit to this, imho, is the Handle handle = DBIUtil.getHandle(dbi); which uses the Spring idiom of having a static helper to get transactionally bound resources. I dislike it, but it is Spring idiom, and I am reusing the jDBI Spring stuff, so... c'est la vie for now.

Setting it up and running, then is just:

    public void setUp() throws Exception
    {
        super.setUp();
        this.guice = Guice.createInjector(new ThingModule(),
                           new SpringTransactionalGuiceModule(Tools.getDataSource()));
    }

    public void testFoo() throws Exception
    {
        final Thing thing = guice.getInstance(Thing.class);
        assertNotNull(thing);

        try {
            thing.excute(new Callback()
            {
                public void call(Handle handle)
                {
                    assertTrue(handle.isInTransaction());
                    handle.insert("insert into something (id, name) values (?, ?)", 1, "Rob");
                    throw new IllegalStateException();
                }
            });
            fail("Should have thrown an exception");
        }
        catch (IllegalStateException e) {
            assertTrue(true);
        }
        final Handle h = openHandle();
        assertFalse(h.isInTransaction());
        final List<String> names = h.createQuery("select name from something")
                .map(StringMapper.FIRST)
                .list();
        assertEquals(0, names.size());
    }

And it all works! Woo hoo!

A drawback, and I haven't found a way around this, is that you cannot provide dependencies to interceptors, so the data source must be available outside the injector. Bob says it will be in the next release though, so woot!

4 writebacks [/src/java] permanent link