技術ネタはQiitaに移りました。壁もどこぞに。

DBUnit をそのまま使うと ORACLE のシノニムが使えない

表題通りである。(二度目。) DBUnit は、何もせずに使うとシノニムを検出できない。これは、以前紹介した以下エントリの通り、DBUnit が DB のメタデータを必要とするのに影響する。

yo1000.hateblo.jp

シノニムが使用できない理由として、DBUnitメタデータチェックがある。DBUnit は、取得したメタデータとテストデータの照合を行う際に、実テーブルのカラム一覧を取得しなければならないのだが、ここでシノニムがテーブルとみなされず、テストデータで列挙されたカラムがメタデータ内に見つからない、という例外をスローしてしまうのである。

これを、メタデータとして DBUnit に検出させるためには、IDatabaseTester#getConnection() をオーバーライドして、DatabaseConfig.PROPERTY_TABLE_TYPE を設定してやる必要がある。

また、これだけでは完全ではなく、以下のようなシノニムの張り方をしていた場合、さらに DatabaseConfig.PROPERTY_METADATA_HANDLER も設定しなければならない。

f:id:Yoichi-KIKUCHI:20150922154313p:plain

ただし、このような要件に合致する IMetadataHandler の実装は用意されていないため、こちらは自作する必要がある。これらを解消するためのコードは以下のようになる。(例によって、Spring Boot での Bean 定義で使用した場合のコードサンプル。)

DatabaseTester 側の設定コード。

// 中略

  @Bean
  @Autowired
  public DataSourceDatabaseTester tester(DataSource dataSource,
      @Value("${spring.datasource.schema}") String schema) throws Exception {

    DataSourceDatabaseTester tester = new DataSourceDatabaseTester(
        new TransactionAwareDataSourceProxy(dataSource)) {
      @Override public IDatabaseConnection getConnection() throws Exception {
        IDatabaseConnection conn = super.getConnection();

        conn.getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new OracleDataTypeFactory());
        conn.getConfig().setProperty(DatabaseConfig.PROPERTY_TABLE_TYPE, new String[] {"TABLE", "ALIAS", "SYNONYM"});
        conn.getConfig().setProperty(DatabaseConfig.PROPERTY_METADATA_HANDLER, new OracleResolvedSynonymMetaDataHandler());

        return conn;
      }
    };

    tester.setSchema(schema);
    tester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
    return tester;
  }

// 中略

自作した IMetadataHandler のコード。

// The MIT License (MIT)
// 
// Copyright (c) 2015 Yoichi KIKUCHI
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package com.yo1000.example.component.support.dbunit.oracle;

import org.dbunit.database.DefaultMetadataHandler;

import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * Created by Yoichi.KIKUCHI on 2015/09/15.
 */
public class OracleResolvedSynonymMetaDataHandler extends DefaultMetadataHandler {
    protected static class ResolvedSynonym {
        private String owner;
        private String tableName;

        public ResolvedSynonym() {}

        public ResolvedSynonym(String owner, String tableName) {
            this.setOwner(owner);
            this.setTableName(tableName);
        }

        public String getOwner() {
            return owner;
        }

        public void setOwner(String owner) {
            this.owner = owner;
        }

        public String getTableName() {
            return tableName;
        }

        public void setTableName(String tableName) {
            this.tableName = tableName;
        }
    }

    private PreparedStatement cachedUserObjectsStatement;
    private PreparedStatement cachedAllSynonymsStatement;

    public PreparedStatement getCachedUserObjectsStatement() {
        return cachedUserObjectsStatement;
    }

    public void setCachedUserObjectsStatement(PreparedStatement cachedUserObjectsStatement) {
        this.cachedUserObjectsStatement = cachedUserObjectsStatement;
    }

    public PreparedStatement getCachedAllSynonymsStatement() {
        return cachedAllSynonymsStatement;
    }

    public void setCachedAllSynonymsStatement(PreparedStatement cachedAllSynonymsStatement) {
        this.cachedAllSynonymsStatement = cachedAllSynonymsStatement;
    }

    @Override public ResultSet getColumns(DatabaseMetaData databaseMetaData, String schemaName,
            String tableName) throws SQLException {
        this.setCachedUserObjectsStatement(databaseMetaData.getConnection()
                .prepareStatement("select OBJECT_TYPE from USER_OBJECTS where OBJECT_NAME like ?"));

        this.setCachedAllSynonymsStatement(databaseMetaData.getConnection().prepareStatement(
                "select TABLE_OWNER, TABLE_NAME from ALL_SYNONYMS where OWNER like ? and SYNONYM_NAME like ?"));

        ResolvedSynonym resolvedSynonym = this.resolveSynonym(schemaName, tableName);

        PreparedStatement statement = databaseMetaData.getConnection().prepareStatement("select "
                + "    NULL as table_cat, "
                + "    ? as table_schem, "
                + "    ? as table_name, "
                + "    column_name as column_name, "
                + "    DECODE (data_type, "
                + "        'CHAR'        ,     1, 'VARCHAR2'    ,     12, 'NUMBER'            ,        3, "
                + "        'LONG'        ,    -1, 'DATE'            ,     93, 'RAW'                 ,     -3, "
                + "        'LONG RAW',    -4, 'BLOB'            , 2004, 'CLOB            '    , 2005, "
                + "        'BFILE'     , -13, 'FLOAT'         ,        6, 'TIMESTAMP(6)',     93, "
                + "        'TIMESTAMP(6) WITH TIME ZONE'            , -101, "
                + "        'TIMESTAMP(6) WITH LOCAL TIME ZONE', -102, "
                + "        'INTERVAL YEAR(2) TO MONTH'                , -103, "
                + "        'INTERVAL DAY(2) TO SECOND(6)'         , -104, "
                + "        'BINARY_FLOAT', 100, 'BINARY_DOUBLE', 101, 1111) as data_type, "
                + "    data_type as type_name, "
                + "    DECODE (data_precision, "
                + "        null, DECODE ("
                + "            data_type    , 'CHAR'         , char_length, 'VARCHAR', char_length, 'VARCHAR2',"
                + "            char_length, 'NVARCHAR2', char_length, 'NCHAR'    , char_length, data_length"
                + "        ), "
                + "        data_precision) as column_size, "
                + "    0 as buffer_length, "
                + "    data_scale as decimal_digits, "
                + "    10 as num_prec_radix, "
                + "    DECODE (nullable, 'N', 0, 1) as nullable, "
                + "    NULL as remarks, "
                + "    data_default as column_def, "
                + "    0 as sql_data_type, "
                + "    0 as sql_datetime_sub, "
                + "    data_length as char_octet_length, "
                + "    column_id as ordinal_position, "
                + "    DECODE (nullable, 'N', 'NO', 'YES') as is_nullable "
                + "from "
                + "    ALL_TAB_COLUMNS "
                + "where "
                + "    OWNER like ? "
                + "and "
                + "    TABLE_NAME like ?");

        statement.clearParameters();
        statement.setString(1, schemaName);
        statement.setString(2, tableName);
        statement.setString(3, resolvedSynonym.getOwner());
        statement.setString(4, resolvedSynonym.getTableName());

        return statement.executeQuery();
    }

    protected boolean isSynonym(String objectName) throws SQLException {
        PreparedStatement statement = this.getCachedUserObjectsStatement();
        statement.clearParameters();
        statement.setString(1, objectName);
        ResultSet resultSet = statement.executeQuery();

        if (!resultSet.next()) {
            return false;
        }

        String objectType = resultSet.getString("OBJECT_TYPE");

        if (objectType == null) {
            throw new IllegalStateException(String.format(
                    "Target object type is unknown. objectName: %s",
                    objectName));
        }

        return objectType.equalsIgnoreCase("SYNONYM");
    }

    protected ResolvedSynonym resolveSynonym(String schemaName, String objectName)
            throws SQLException {
        if (!this.isSynonym(objectName)) {
            return new ResolvedSynonym(schemaName, objectName);
        }

        PreparedStatement statement = this.getCachedAllSynonymsStatement();
        statement.clearParameters();
        statement.setString(1, schemaName);
        statement.setString(2, objectName);
        ResultSet resultSet = statement.executeQuery();

        if (!resultSet.next()) {
            return new ResolvedSynonym(schemaName, objectName);
        }

        String resolvedSchemaName = resultSet.getString("TABLE_OWNER");
        String resolvedObjectName = resultSet.getString("TABLE_NAME");

        return this.resolveSynonym(resolvedSchemaName, resolvedObjectName);
    }
}

処理としてはそう難しいものではなく、実装した IMetadataHandler では、オブジェクトがシノニムであるかぎり、シノニム定義を管理するテーブルを繰り返し検索し、対象の実テーブルが見つかった時点で、カラムの一覧を取得する。

ただし、取得したカラムの一覧と、テストデータで指定されたテーブル名の組み合わせを合致させるため、ALL_TAB_COLUMNS への問い合わせでは、table_schem と、table_name の値を、シノニム解決を始めたときに最初に問い合わせたシノニムの名前で返してやっている。

こうすることで、DBUnit に与えたテストデータで、シノニム名と、実テーブルのカラム名を使用しても、DB メタデータとの照合に成功し、テストデータの処理が問題なく行われるようになる。

しかし、検索してもストレートな解決策を提示している記事が見つからなかったのだけど、他の皆さんはどのように解決しているのだろうか……。