Поддержка транзакций, тестового окружения и другие удобные надстройки над 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-ы результирующих классов.
Автор: gkislin