Поддержка транзакций, тестового окружения и другие удобные надстройки над commons-dbutils.
"/>
Если обращение к RDBMS вашего проекта ведется не через ORM или Spring JdbcTemplate, то работу через голый JDBC может сильно скрасить commons-dbutils.
Особенно если его надстроить транзакциями, работой с разными окружениями, авто-открытием/закрытием ресурсов, запрсами read-only, обработкой SqlException, возвратом pk при вставке и т.д.
Т.е, например, чтобы можно было бы сделать так:
int userId = Sql.queryForValue("SELECT ID FROM OU.USERS WHERE LAST_NAME=?", "IVANOV"); Sql.update("DELETE FROM OU.USERS WHERE FIRST_NAME=? AND LAST_NAME=?", fname, lname); List<User> list = Sql.query("SELECT FROM OU.USERS", USER_LIST_HANDLER); int newId = Sql.insert("INSERT INTO OU.USERS VALUES (?,?)", fname, lname);
И этот код работал бы внутри транзакции, через Application Server Connction pool и в JUnit без изменений и mock-ов.
За чуть меньше года моей работы с commons-dbutils он оброс различными классами и утильными методами, о которых и хочу здесь рассказать.
Работа в разных окружениях
Используя Инициализацию по тербованию через интерфейс DataSourceFactory получаем DataSource, объявленный в JNDI AS:
public class ConnectionFactory { private static final LoggerWrapper LOGGER = LoggerWrapper.get(ConnectionFactory.class); private static class JndiDataSource { private static final DataSource dataSource; static { try { InitialContext ctx = new InitialContext(); dataSource = (DataSource) ctx.lookup("java:/comp/env/jdbc/db_name"); } catch (Exception ex) { throw LOGGER.getIllegalStateException("PostgreSQL initialization failed", ex); } } } interface DataSourceFactory { DataSource getDataSource(); } static DataSourceFactory dataSourceFactory = new DataSourceFactory() { @Override public DataSource getDataSource() { return JndiDataSource.dataSource; } }; public static DataSource getDataSource() { return dataSourceFactory.getDataSource(); }
Для тестов подменяем его на commons-dbcp:
<dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency>
public class TestSq { private static String DB_URL; private static String DB_USER; private static String DB_PSW; private static class DbcpDataSource { private static final BasicDataSource dataSource; static { dataSource = new BasicDataSource(); dataSource.setUrl(DB_URL); dataSource.setUsername(DB_USER); dataSource.setPassword(DB_PSW); } } public static void initDb(String dbUrl, String dbUser, String dbPsw) { DB_URL = dbUrl; DB_USER = dbUser; DB_PSW = dbPsw; ConnectionFactory.dataSourceFactory = new ConnectionFactory.DataSourceFactory() { @Override public DataSource getDataSource() { return DbcpDataSource.dataSource; } }; }
Если креденшелы к базе находятся в профилях maven settings.xml, то можно инициализировать тесты, основываясь на maven profile.
Создаем test/resources/project.properties c креденшелами базы:
db.url=${db.url} db.user=${db.user} db.password=${db.password}
Фильтруем его переменными из settings:
<testResources> <testResource> <filtering>true</filtering> </testResource>
и загружаем properties через getResourceAsStream:
public static void initProfileDb() { Properties prop = UtilProperties.loadAsResource("project.properties"); initDb(prop.getProperty("db.url"), prop.getProperty("db.user"), prop.getProperty("db.password")); }
Остается в начале тестов инициализировать TestSql.initProfileDb() и все запросы к базе выбранного профиля (mvn -P env test) будут идти через commons-dbcp.
Работа с ресурсами и исключениями
Добавляем в ConnectionFactory получение коннекта и его закрытие:
static Connection getConnection(boolean isReadOnly) throws SQLException { Connection conn = getDataSource().getConnection(); if (isReadOnly) { conn.setReadOnly(true); } return conn; } static void close(Connection conn) { if (conn != null) { try { if (conn.isReadOnly()) { conn.setReadOnly(false); // restore NOT readOnly before return to pool } conn.close(); } catch (SQLException e) { Sql.warn(e); } } }
Объявляем интерфейс где собственно будет логика запроса
public interface SqlExecutor<T> { T run(Connection conn) throws SQLException; }
и его обертку:
public class Sql { public static <T> T execute(boolean isReadOnly, SqlExecutor<T> executor) { try { return executor.run(ConnectionFactory.getConnection(isReadOnly) } catch (SQLException e) { throw LOGGER.getStateException(ExceptionType.DATA_BASE, e); } finally { ConnectionFactory.close(conn); } }
Теперь все запросы к базе будут идти через нашу обертку:
private static final QueryRunner QUERY_RUNNER = new QueryRunner(); public static int update(Connection conn, final String updateSql, final Object... params) throws SQLException { return QUERY_RUNNER.update(conn, updateSql, params); } public static <T> T query(Connection conn, final String sql, final ResultSetHandler<T> rsh, final Object... params) throws SQLException { return QUERY_RUNNER.query(conn, sql, rsh, params); } public static int update(final String updateSql, final Object... params) { return Sql.execute(false, new SqlExecutor<Integer>() { @Override public Integer run(Connection conn) throws SQLException { return update(conn, updateSql, params); } }); } public static <T> T query(final String sql, final ResultSetHandler<T> rsh, final Object... params) { return execute(true, new SqlExecutor<T>() { @Override public T run(Connection conn) throws SQLException { return query(conn, sql, rsh, params); } }); }
Скаляры достаем через утильные методы:
public static <T> T queryForValue(Connection conn, final String sql, final Object... params) throws SQLException { return query(conn, sql, new ScalarHandler<T>(), params); } public static <T> T queryForValue(final String sql, final Object... params) throws SQLException { return query(sql, new ScalarHandler<T>(), params); }
Получение PK при вставке
Пока Generated key handling for updates в статусе Unresolved, приходится делать все самим:
public class KeyQueryRunner extends QueryRunner { private static final LoggerWrapper LOGGER = LoggerWrapper.get(KeyQueryRunner.class); private static final ResultSetHandler<Integer> KEY_HANDLER = new ScalarHandler<Integer>(); public int insert(Connection conn, String sql, Object... params) throws SQLException { PreparedStatement stmt = null; int rows = 0; try { stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); this.fillStatement(stmt, params); rows = stmt.executeUpdate(); if (rows != 1) { throw LOGGER.getStateException("Ошибка вставки записи " + sql, ExceptionType.DATA_BASE); } return KEY_HANDLER.handle(stmt.getGeneratedKeys()); } catch (SQLException e) { this.rethrow(e, sql, params); } finally { close(stmt); } return rows; } } private static final KeyQueryRunner KEY_QUERY_RUNNER = new KeyQueryRunner(); public static int insert(Connection conn, final String insertSql, final Object... params) throws SQLException { return KEY_QUERY_RUNNER.insert(conn, insertSql, params); } public static int insert(final String insertSql, final Object... params) { return Sql.execute(false, new SqlExecutor<Integer>() { @Override public Integer run(Connection conn) throws SQLException { return insert(conn, insertSql, params); } }); }
Если ключи типа long — не забудте заменить int->long, Integer->Long.
Транзакционность
Наконец, какая работа с базой без транзакций?
Допустим, у нас есть какой-то метод, обращающийся к базе:
checkAssess(final int docId, final Access accessMode)
Connection он не принимает, а нам захотелось использовать его внутри транзакции.
И хотелось бы не дублировать его в
checkAssess(Connection conn, final int docId, final Access accessMode)
Делаем транзакционную обертку:
public static <T> T executeInTransaction(SqlExecutor<T> executor) { Connection conn = null; try { conn = ConnectionFactory.getTxConnection(); T res = executor.run(conn); conn.commit(); return res; } catch (Error e) { throw rollback(conn, e); } catch (Exception e) { throw rollback(conn, e); } finally { ConnectionFactory.closeTx(conn); } } private static StateException rollback(Connection conn, Throwable e) { try { if (conn != null) { conn.rollback(); } return LOGGER.getStateException(ExceptionType.DATA_BASE, e); } catch (SQLException se) { return LOGGER.getStateException("Unable to rollback transaction", ExceptionType.DATA_BASE, e); } }
и кладем коннекшен транзакции вThreadLocal:
public class ConnectionFactory { ... private static final ThreadLocal<Connection> TRANSACT_CONN = new ThreadLocal<Connection>(); static Connection getCurrentConn() { return TRANSACT_CONN.get(); } static Connection getTxConnection() throws SQLException { Connection conn = TRANSACT_CONN.get(); if (conn != null) { throw LOGGER.getIllegalStateException("Start second transaction in one thread"); } conn = getDataSource().getConnection(); conn.setAutoCommit(false); TRANSACT_CONN.set(conn); return conn; } static void closeTx(Connection conn) { close(conn); TRANSACT_CONN.set(null); }
И учитываем возможность транзакции в нашем Execute:
public static <T> T execute(boolean isReadOnly, SqlExecutor<T> executor) { Connection conn = null; Connection txConn = ConnectionFactory.getCurrentConn(); try { return executor.run( (txConn == null) ? ConnectionFactory.getConnection(isReadOnly) : txConn); } catch (SQLException e) { throw LOGGER.getStateException(ExceptionType.DATA_BASE, e); } finally { if (txConn == null) ConnectionFactory.close(conn); } }
Теперь любой наш запрос к базе через Execute внутри транзакции будет работать с транзакционным коннектом.
Но также останется возможность внутри транзакции обращаться к базе не через Executor:
List<Person> list = Sql.executeInTransaction(new SqlExecutor<List<Person>>() { @Override public List<Person> run(Connection conn) throws SQLException { Sql.select(conn, ...); Sql.update(conn, ...); return Sql.select(conn, ...); } });
Подход простой по сравнению, например, с Spring TransactionManager, нет вложенных транзакций, но для 99% случаев этого достаточно. Особенно если не хочется тянуть за собой весь Spring или ORM.
Наконец, чтобы не писать тип Void и return null, сделаем возможность запросов без возвращения результата (аналог Spring TransactionCallbackWithoutResult)
public interface SqlExecutorVoid { void run(Connection conn) throws SQLException; } public class Sql { ... private static SqlExecutor<Void> getWrapperExecutor(final SqlExecutorVoid voidExecutor) { return new SqlExecutor<Void>() { @Override public Void run(Connection conn) throws SQLException { voidExecutor.run(conn); return null; } }; } public static void execute(boolean isReadOnly, SqlExecutorVoid executor) { execute(isReadOnly, getWrapperExecutor(executor)); } public static void executeInTransaction(SqlExecutorVoid executor) { executeInTransaction(getWrapperExecutor(executor)); }
Если Вам понравился подход, то собрав из кусков классы вы будете иметь удобный (и уже знакомый) инструментарий для несложной работы с вашей базой.
Спасибо за внимание. Ссылки на проекты, которые уже реализуют нечто похожее и конструктивная критика приветствуется.
PS: при использовании BeanHandler и BeanListHandler маппинга ResultSet в бины java не забывайте про конструктор по умолчанию и setter-ы результирующих классов.
ссылка на оригинал статьи http://habrahabr.ru/post/183204/
Добавить комментарий