Transactions with Guice and JDBC - Solution discussion
Asked Answered
F

0

6

In my application, I need to use pure JDBC together with Guice. However, Guice doesn't provide any built-in support to manage transactions. guice-persist only provides support based on JPA, which I cannot use.

so I tried to implement a simple solution to manage transactions with Guice and JDBC. here is the first version:

  1. use TransactionHolder to store the transaction per thread.

    public class JdbcTransactionHolder {

    private static ThreadLocal<JdbcTransaction> currentTransaction = new ThreadLocal<JdbcTransaction>();
    
    public static void setCurrentTransaction(JdbcTransaction transaction) {
        currentTransaction.set(transaction);
    }
    
    public static JdbcTransaction getCurrentTransaction() {
        return currentTransaction.get();
    }
    
    public static void removeCurrentTransaction() {
        currentTransaction.remove();
    }
    

    }

  2. implements a transaction manager for JDBC, for now only begin(), getTransaction(), commit() and rollback() method:

    public class JdbcTransactionManager implements TransactionManager {

    @Inject
    private DataSource dataSource;
    
    @Override
    public void begin() throws NotSupportedException, SystemException {
        logger.debug("Start the transaction");
        try {
            JdbcTransaction tran = JdbcTransactionHolder.getCurrentTransaction();
            Connection conn = null;
            if(tran == null) {
                conn = dataSource.getConnection();
            }
            else {
                conn = tran.getConnection();
            }
    
            // We have to put the connection in the holder so that we can get later
            // from the holder and use it in the same thread
            logger.debug("Save the transaction for thread: {}.", Thread.currentThread());
            JdbcTransactionHolder.setCurrentTransaction(new JdbcTransaction(conn));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    
    }
    
    @Override
    public void commit() throws RollbackException, HeuristicMixedException,
            HeuristicRollbackException, SecurityException,
            IllegalStateException, SystemException {
        logger.debug("Commit the transaction");
        try {
            logger.debug("Get the connection for thread: {}.", Thread.currentThread());
            Transaction transaction = JdbcTransactionHolder.getCurrentTransaction();
            transaction.commit();
    
        }
        catch(Exception e) {
            throw new RuntimeException(e);
        }
        finally {
            JdbcTransactionHolder.removeCurrentTransaction();
        }
    }
    
    @Override
    public Transaction getTransaction() throws SystemException {
        logger.debug("Get transaction.");
        final JdbcTransaction tran = JdbcTransactionHolder.getCurrentTransaction();
        if(tran == null) {
            throw new DBException("No transaction is availble. TransactionManager.begin() is probably not yet called.");
        }
    
        return tran;
    }
    
    @Override
    public void rollback() throws IllegalStateException, SecurityException,
            SystemException {
        logger.debug("Rollback the transaction");
    
        try {
            logger.debug("Get the transaction for thread: {}.", Thread.currentThread());
            Transaction conn = JdbcTransactionHolder.getCurrentTransaction();
            conn.commit();
        }
        catch(Exception e) {
            throw new RuntimeException(e);
        }
        finally {
            JdbcTransactionHolder.removeCurrentTransaction();
        }
    }
    

    }

  3. implement a wrapper for DataSource which can get the current connection from the transaction holder if a transaction has been started:

    public class JdbcDataSource implements DataSource {

    private final static org.slf4j.Logger logger = LoggerFactory.getLogger(JdbcDataSource.class);
    
    private DataSource dataSource;
    
    public JdbcDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return dataSource.getLogWriter();
    }
    
    @Override
    public int getLoginTimeout() throws SQLException {
    
        return dataSource.getLoginTimeout();
    }
    
    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
    
        return dataSource.getParentLogger();
    }
    
    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
        this.dataSource.setLogWriter(out);
    }
    
    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        this.dataSource.setLoginTimeout(seconds);
    }
    
    @Override
    public boolean isWrapperFor(Class<?> arg0) throws SQLException {
        return this.isWrapperFor(arg0);
    }
    
    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
    
        return this.unwrap(iface);
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        JdbcTransaction transaction = JdbcTransactionHolder.getCurrentTransaction();
        if(transaction != null) {
            // we get the connection from the transaction
            logger.debug("Transaction exists for the thread: {}.", Thread.currentThread());
            return transaction.getConnection();
        }
        Connection conn = this.dataSource.getConnection();
        conn.setAutoCommit(false);
        return conn;
    }
    
    @Override
    public Connection getConnection(String username, String password)
            throws SQLException {
        JdbcTransaction transaction = JdbcTransactionHolder.getCurrentTransaction();
        if(transaction != null) {
            // we get the connection from the transaction
            logger.debug("Transaction exists for the thread: {}.", Thread.currentThread());
            return transaction.getConnection();
        }
    
        return this.dataSource.getConnection(username, password);
    }
    

    }

  4. then create a DataSourceProvider so that we can inject DataSource to any POJO using guice:

    public class DataSourceProvider implements Provider {

    private static final Logger logger = LoggerFactory.getLogger(DataSourceProvider.class);
    
    private DataSource dataSource;
    
    
    public DataSourceProvider() {
    
        JdbcConfig config = getConfig();
        ComboPooledDataSource pooledDataSource = new ComboPooledDataSource();
    
        try {
            pooledDataSource.setDriverClass(config.getDriver());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    
        pooledDataSource.setJdbcUrl(config.getUrl());
        pooledDataSource.setUser(config.getUsername());
        pooledDataSource.setPassword(config.getPassword() );
        pooledDataSource.setMinPoolSize(config.getMinPoolSize());
        pooledDataSource.setAcquireIncrement(5);
        pooledDataSource.setMaxPoolSize(config.getMaxPoolSize());
        pooledDataSource.setMaxStatements(config.getMaxStatementSize());
        pooledDataSource.setAutoCommitOnClose(false);
    
        this.dataSource = new JdbcDataSource(pooledDataSource);
    }
    
    
    private JdbcConfig getConfig() {
    
        JdbcConfig config = new JdbcConfig();
        Properties prop = new Properties();
        try {
            //load a properties file from class path, inside static method
            prop.load(JdbcConfig.class.getResourceAsStream("/database.properties"));
    
            //get the property value and print it out
            config.setDriver(prop.getProperty("driver"));
            config.setUrl(prop.getProperty("url"));
            config.setUsername(prop.getProperty("username"));
            config.setPassword(prop.getProperty("password"));
    
            String maxPoolSize = prop.getProperty("maxPoolSize");
            if(maxPoolSize != null) {
                config.setMaxPoolSize(Integer.parseInt(maxPoolSize));
            }
    
            String maxStatementSize = prop.getProperty("maxStatementSize");
            if(maxStatementSize != null) {
                config.setMaxStatementSize(Integer.parseInt(maxStatementSize));
            }
    
            String minPoolSize = prop.getProperty("minPoolSize");
            if(minPoolSize != null) {
                config.setMinPoolSize(Integer.parseInt(minPoolSize));
            }
        } 
        catch (Exception ex) {
            logger.error("Failed to load the config file!", ex);
            throw new DBException("Cannot read the config file: database.properties. Please make sure the file is present in classpath.", ex);
        }
    
        return config;
    }
    
    @Override
    public DataSource get() {
        return dataSource;
    }
    
  5. and then implement TransactionalMethodInterceptor to manage the transaction for the method with Transactional annotation:

    public class TransactionalMethodInterceptor implements MethodInterceptor {

    private final static Logger logger = LoggerFactory.getLogger(TransactionalMethodInterceptor.class);
    
    @Inject
    private JdbcTransactionManager transactionManager;
    
    @Override
    public Object invoke(MethodInvocation method) throws Throwable {
    
        try {
            // Start the transaction
            transactionManager.begin();
            logger.debug("Start to invoke the method: " + method);
    
            Object result = method.proceed();
            logger.debug("Finish invoking the method: " + method);
            transactionManager.commit();
            return result;
        } catch (Exception e) {
            logger.error("Failed to commit transaction!", e);
            try {
                transactionManager.rollback();
    
            }
            catch(Exception ex) {
                logger.warn("Cannot roll back transaction!", ex);
            }
            throw e;
        }
    }
    

    }

  6. Finally, the code to put all together so that Guice can inject the instances:

    bind(DataSource.class).toProvider(DataSourceProvider.class).in(Scopes.SINGLETON);
    
    bind(TransactionManager.class).to(JdbcTransactionManager.class);
    TransactionalMethodInterceptor transactionalMethodInterceptor = new TransactionalMethodInterceptor();
    requestInjection(transactionalMethodInterceptor);
    bindInterceptor(Matchers.any(), Matchers.annotatedWith(Transactional.class), transactionalMethodInterceptor);
    
    
    bind(TestDao.class).to(JdbcTestDao.class);
    bind(TestService.class).to(TestServiceImpl.class);
    

I use c3p0 for the datasource pool. so, it works just fine in my test.

I find another related question: Guice, JDBC and managing database connections

but so far I haven't find any similar approach, except something in SpringFramework. but even the implementation in Spring seems quite complex.

I would like to ask if anyone has any suggestion for this solution.

thanks.

Franklin answered 4/2, 2015 at 9:10 Comment(1)
one thing in mind: instead of using RuntimeException, the exception needs to be defined specific to handle errors.Franklin

© 2022 - 2024 — McMap. All rights reserved.