【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の定義です。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN"
 
<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を使ってエクセルファイルからテーブルデータをセットアップします。

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
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()]);
    }
 
}

エラー内容

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[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 を設定しているところを修正します。

1
2
3
4
5
6
7
8
9
<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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

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