commons-dbutilsを介したトランザクション、テスト環境、その他の便利なアドオンのサポート

commons-dbutilsを介したトランザクション、テスト環境、その他の便利なアドオンのサポート



「/>

プロジェクトへのRDBMSアクセスがORMまたはSpring JdbcTemplateを介して行われない場合、ベアJDBCを介して作業すると、 commons-dbutilsが大幅に明るくなります。

特に、トランザクションで追加したり、さまざまな環境で作業したり、リソースを自動的に開いたり閉じたり、読み取り専用の要求をしたり、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);
      
      





そして、このコードはトランザクション内で動作し、アプリケーションサーバー接続プールを介して、変更やモックなしでJUnitで動作します。

commons-dbutilsを使った私の仕事の1年未満で、彼はさまざまなクラスと便利なメソッドを取得しました。これについては、ここで説明します。



さまざまな環境で働く



DataSourceFactoryインターフェースを介したクレーム初期化を使用して、JNDI ASで宣言されたDataSourceを取得します。

 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プロファイルに基づいてテストを初期化できます。

データベースの資格情報を使用してtest / resources / project.propertiesを作成します。

  db.url=${db.url} db.user=${db.user} db.password=${db.password}
      
      





設定の変数を使用してフィルタリングします。

  <testResources> <testResource> <filtering>true</filtering> </testResource>
      
      





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); } }); }
      
      





scrapメソッドを使用してスカラーを取得します。

 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を取得する



未解決ステータスの更新の生成されたキーの処理中は、すべてを自分で行う必要があります。

 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型を書き込まずに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 Beanにマッピングする場合、結果のクラスのデフォルトのコンストラクターとセッターを忘れないでください。



All Articles