【spring batch】dbUnitを使用してpostgresqlの特殊データ型にセットアップする

【spring batch】dbUnitを使用してpostgresqlの特殊データ型にセットアップする

postgresqlを使用したdbUnitテストを行った時に、DBUnitのデフォルトのDataFactoryであるPostgresqlDataTypeFactoryを使用すると思うのですが、postgresqlのテーブルの中に特殊データ型に該当するカラムがあると上手く持ってこれない事象が発生しました。

特殊データ型というのは一般的なDBにあるような型(int,timestamp,varcharとか)ではなく、postgresql特有で定義されている型の事です。(_text、tsvectorとか)
https://www.postgresql.jp/document/9.4/html/datatype.html

事象

Spring batchのRepositoryTestを特殊データ型のカラムが含まれているテーブルに対してdbUnitを用いてSetupしようとしたときにエラーが発生。

対象のテーブル

postgresql公式のチュートリアルにあるテーブル群を使っています。
PostgreSQL Sample Database
今回はfilmテーブルからのSELECTをテストします。

filmテーブル

この中でpostgresqlの特殊データ型に属するのは

  • mpaa_rating
  • _text
  • tsvector

になります。それ以外は通常のやり方でJavaとのマッピングが可能です。
http://beanql.osdn.jp/type_map.html

SQL

Mybatisの定義です。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="itouoti.spring.batch.repository.jobsRepository.job1.Job1Repository">

    <select id="sarchfilmData" resultType="itouoti.spring.batch.repository.commonDTO.FilmDTO" parameterType="map">
        <![CDATA[
            SELECT
                 film_id AS filmId,
                 title,
                 description,
                 release_year AS releaseYear,
                 language_id AS languageId,
                 rental_duration AS rentalDuration,
                 rental_rate AS rentalRate,
                 length,
                 replacement_cost AS replacementCost,
                 rating,
                 last_update AS lastUpdate,
                 special_features AS specialFeatures,
                 fulltext
            FROM
                film
            WHERE
                film_id = #{filmId}
        ]]>
    </select>

</mapper>

テストコード

DBUnitを使ってエクセルファイルからテーブルデータをセットアップします。

package itouoti.spring.batch.repository.jobsRepository.job1;

import *;

/**
 * Theoriesテスト
 */
@RunWith(Theories.class)
@ContextConfiguration(locations = { "classpath:META-INF/jobs/job1/job1Test.xml" })
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@Transactional
@DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class)
@Slf4j
/**
 * job1Repository sarchfilmData Test
 * @author itouoti
 */
public class Job1RepositorySerchFilmDataTest {

    @ClassRule
    public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    @Inject
    Job1Repository target;

    @Inject
    ResourceLoader resourceLoader;

    @Inject
    JdbcTemplate jdbcTemplate;

    @Inject
    TransactionAwareDataSourceProxy masterDataSource;

    @Value("${xlsx.expectedDataFile}")
    String xlsxExpectedDataFile;

    // エクセルファイルパス
    private static String xlsxPath = "classpath:itouoti/spring/batch/repository/jobsRepository/job1/";

    // テストパターン用エクセルオブジェクト
    private static IDataSet xlsDataSet;

    // assertエラー格納用List
    private static List<AssertionError> assertionErrors = new ArrayList<AssertionError>();

    private static String jdbcProps = "classpath:batch-application.properties";

    private static Properties props;

    /**
     * テストメソッド起動前設定
     * @throws Exception 例外
     */
    @BeforeClass
    public static void setUpBeforeTest() throws Exception {

        log.info("□sarchfilmData test is start");

        // xlsxファイルの読み込み
        File file = ResourceUtils.getFile(xlsxPath + "testList.xlsx");
        xlsDataSet = new XlsDataSet(file);

        // propertiesファイルの読み込み
        props = LoadProperties.load(jdbcProps);

        // 外部キー制約の解除
        String disableAlter = "ALTER TABLE film DISABLE TRIGGER ALL";

        ExecSQL.execSQLExecSQL(props, disableAlter, false);
    }

    /**
     * テスト毎起動前設定
     * @throws Exception 例外
     */
    @Before
    public void setUp() throws Exception {

    }

    /**
     * テスト毎検証後処理
     */
    @After
    public void cleanUp() {
        // テスト後の後処理があれば記述
    }

    /**
     * テストメソッド実行後処理
     * @throws Exception 例外
     */
    @AfterClass
    public static void creanUpAfterTest() throws Exception {
        // テスト後の後処理があれば記述
        if (assertionErrors.size() > 0) {
            for (AssertionError e : assertionErrors) {
                log.error(e.getMessage());
                e.printStackTrace();
            }
            log.error(assertionErrors.size() + "件のAssertionErrorが発生しています");
            fail(assertionErrors.size() + "件のAssertionErrorが発生しています");
        }

        log.info("□sarchfilmData test is end");

        // 外部キー制約の設定
        String disableAlter = "ALTER TABLE film ENABLE TRIGGER ALL";

        ExecSQL.execSQLExecSQL(props, disableAlter, false);
    }

    /**
     * job1Repository.sarchfilmDataテスト
     * @throws Exception DBUnitのxlsx読み込み例外
     */
    @Theory
    @DatabaseSetup("classpath:itouoti/spring/batch/repository/jobsRepository/job1/setUp_data.xlsx")
    public void sarchfilmDataTest(
                                  Job1RepositorySerchFilmDataParam testParam) throws Exception {

        log.info("■ TestNo." + testParam.no + " sarchfilmData test is start");

        try {

            // STEP1 メソッドのテスト
            Map<String, Object> map = new HashMap<>();
            map.put("filmId", testParam.getFilmId());
            FilmDTO result = target.sarchfilmData(map);

            // STEP2 取得件数の検証
            assertThat(result.getTitle(), is(testParam.getExpectTitle()));
            assertThat(result.getDescription(), is(testParam.getExpectDescription()));

            // STEP3 テーブル状態の検証
            ITable actual1 = DBUnitSetUp.getAtualDataTable(masterDataSource, "public.film");
            ITable expected1 = DBUnitSetUp.getExpectedDataTable(resourceLoader, xlsxPath + xlsxExpectedDataFile, "film");

            org.dbunit.Assertion.assertEquals(new SortedTable(new TrimTable(expected1)), new SortedTable(new TrimTable(actual1), expected1
                    .getTableMetaData()));

        } catch (AssertionError e) {
            // AssertionErrorsに貯める
            log.error(testParam.no + "Memo = " + testParam.getMemo());
            assertionErrors.add(e);
        } catch (Exception e) {
            // AssertionErrorsに貯める
            log.error(e.getMessage());
            assertionErrors.add(new AssertionError(e));
        } finally {
            log.info("■ TestNo." + testParam.no + " sarchfilmData test is end");
        }
    }

    /**
     * job1Repository.sarchfilmDataテスト用パラメータ生成
     * @return sarchfilmData[]
     * @throws Exception 例外
     */
    @DataPoints
    public static Job1RepositorySerchFilmDataParam[] inputTestParams() throws Exception {

        // テスト用シート名
        String testParamSheet = "param";

        ArrayList<Job1RepositorySerchFilmDataParam> paramList = new ArrayList<Job1RepositorySerchFilmDataParam>();

        try {
            ITable paramTable = xlsDataSet.getTable(testParamSheet);

            for (int i = 0; i < paramTable.getRowCount(); i++) {
                Job1RepositorySerchFilmDataParam param = new Job1RepositorySerchFilmDataParam();

                // 試験No
                param.setNo(TestHelper.toStr(paramTable, i, "no"));

                // filmId
                param.setFilmId(TestHelper.toInt(paramTable, i, "filmId"));

                // 期待値(件数)
                param.setExpectCount(TestHelper.toInt(paramTable, i, "expectCount"));

                // 期待値(title)
                param.setExpectTitle(TestHelper.toStr(paramTable, i, "expectTitle"));

                // 期待値(description)
                param.setExpectDescription(TestHelper.toStr(paramTable, i, "expectDescription"));

                // 備考
                param.setMemo(TestHelper.toStr(paramTable, i, "memo"));

                paramList.add(param);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return paramList.toArray(new Job1RepositorySerchFilmDataParam[paramList.size()]);
    }

}

エラー内容

[2019/06/16 13:47:33] [main] [o.d.util.SQLHelper    ] [WARN ] film.special_features data type (2003, '_text') not recognized and will be ignored. See FAQ for more information.
[2019/06/16 13:47:33] [main] [o.d.util.SQLHelper    ] [WARN ] film.fulltext data type (1111, 'tsvector') not recognized and will be ignored. See FAQ for more information.
[2019/06/16 13:47:33] [main] [o.s.t.c.TestContextManager] [WARN ] Caught exception while allowing TestExecutionListener [com.github.springtestdbunit.DbUnitTestExecutionListener@7ba18f1b] to process 'before' execution of test method [public void itouoti.spring.batch.repository.jobsRepository.job1.Job1RepositorySerchFilmDataTest.sarchfilmDataTest(itouoti.spring.batch.repository.jobsRepository.job1.Job1RepositorySerchFilmDataParam) throws java.lang.Exception] for test instance [itouoti.spring.batch.repository.jobsRepository.job1.Job1RepositorySerchFilmDataTest@581d969c]
org.dbunit.dataset.NoSuchColumnException: film.SPECIAL_FEATURES -  (Non-uppercase input column: special_features) in ColumnNameToIndexes cache map. Note that the map's column names are NOT case sensitive.
	at org.dbunit.dataset.AbstractTableMetaData.getColumnIndex(AbstractTableMetaData.java:117)
	at org.dbunit.operation.AbstractOperation.getOperationMetaData(AbstractOperation.java:89)
	at org.dbunit.operation.AbstractBatchOperation.execute(AbstractBatchOperation.java:150)
	at org.dbunit.operation.CompositeOperation.execute(CompositeOperation.java:79)
	at com.github.springtestdbunit.DbUnitRunner.setupOrTeardown(DbUnitRunner.java:183)
	at com.github.springtestdbunit.DbUnitRunner.beforeTestMethod(DbUnitRunner.java:75)
	at com.github.springtestdbunit.DbUnitTestExecutionListener.beforeTestMethod(DbUnitTestExecutionListener.java:185)
	at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:269)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.RunPrepareTestInstanceCallbacks.evaluate(RunPrepareTestInstanceCallbacks.java:64)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.springframework.test.context.junit4.statements.SpringFailOnTimeout.evaluate(SpringFailOnTimeout.java:87)
	at org.springframework.test.context.junit4.statements.ProfileValueChecker.evaluate(ProfileValueChecker.java:101)
	at org.junit.experimental.theories.Theories$TheoryAnchor$1$1.evaluate(Theories.java:232)
	at org.junit.experimental.theories.Theories$TheoryAnchor.runWithCompleteAssignment(Theories.java:218)
	at org.junit.experimental.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:204)
	at org.junit.experimental.theories.Theories$TheoryAnchor.runWithIncompleteAssignment(Theories.java:212)
	at org.junit.experimental.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:202)
	at org.junit.experimental.theories.Theories$TheoryAnchor.evaluate(Theories.java:187)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.springframework.test.context.junit4.statements.ProfileValueChecker.evaluate(ProfileValueChecker.java:101)
	at org.springframework.test.context.junit4.rules.SpringClassRule$TestContextManagerCacheEvictor.evaluate(SpringClassRule.java:242)
	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)

special_featuresカラムが原因で失敗しています。
また、warnで認識できない型もいくつか警告されています。

原因

dbUnitの標準のpostgresql用のデータファクトリーである「org.dbunit.ext.postgresql.PostgresqlDataTypeFactory」が対応していない型がテーブルに存在するため。

解決策

PostgresqlDataTypeFactoryをextendsして、カスタムファクトリーを作成する。

test-context.xml

まず、 PostgresqlDataTypeFactory を設定しているところを修正します。

<bean
        id="dbUnitDatabaseConfig"
        class="com.github.springtestdbunit.bean.DatabaseConfigBean">
        <property name="datatypeFactory">
            <bean class="itouoti.spring.batch.testCommon.PsqlArrayDataTypeFactory" />
        
            <!-- <bean class="org.dbunit.ext.postgresql.PostgresqlDataTypeFactory" /> -->
        </property>
</bean>

PsqlArrayDataTypeFactory.java

public class PsqlArrayDataTypeFactory extends PostgresqlDataTypeFactory {

    public DataType createDataType(int sqlType,
                                   String sqlTypeName,
                                   String tableName,
                                   String columnName) throws DataTypeException {

        if (sqlType == 2003) {
            if (sqlTypeName.equals("_text")) {
                return new ArrayDataType(sqlTypeName, sqlType, false);
            }
        }

        if (sqlType == 1111) {
            if (sqlTypeName.equals("tsvector")) {
                return new TsVectorDataType(sqlTypeName, sqlType);
            }
        }
        
        if (sqlType == 12) {
            if (sqlTypeName.equals("mpaa_rating")) {
                return new Mpaa_ratingDataType(sqlTypeName, sqlType);
            }
        }

        return super.createDataType(sqlType, sqlTypeName);
    }

}

警告が出ていたSQLTypeとSQLNameの組み合わせが来た時に専用のクラスに飛ばすようにします。

ArrayDataType.java

参考のStackOverFlowを見てください。

TsVectorDataType.java

package itouoti.spring.batch.testCommon.dataType;

import *;

public class TsVectorDataType extends AbstractDataType {

    public TsVectorDataType(String name,
                            int sqlType) {
        super("tsvector", Types.OTHER, String.class, false);
    }

    public Object typeCast(Object value) throws TypeCastException  {
        if (value == null) {
            return null;
        }

        PGobject obj = new PGobject();
        obj.setType("tsvector");
        try {
            obj.setValue(value.toString());
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return obj;
    }

    public Object getSqlValue(int column,
                              ResultSet resultSet) throws SQLException, TypeCastException {
        return resultSet.getString(column);
    }

}

文字列を無理やりセットしています。
Mpaa_ratingDataTypeについてもやり方は同じなので詳しくはgithubソースを参照してください。

ちなみにセットアップデータのエクセルはこんな感じです。

結果

成功しました。

ソース

https://github.com/itouoti12/spring-batch

参考

dbUnit support for Postgresql Array

エラー・バグ対処カテゴリの最新記事