皆さんはDBのユニットテストを行うとき、どのようにテストのセットアップを行っていますか。
私はSimpleJdbcInsertを使用しています。テーブル名とInsert対象のカラムと値のマップを渡すだけで更新してくれるので、個人的には使いやすいと思っています。
しかし、Insert対象のカラムが30-40項目あるテーブルのマップを20個分作成すると、非生産的な上に手間がかかります。
Selectするときは、Responseのクラスを渡してあげるとマッピングしてくれるので、Insertする際も同様の機能があるはずと思っていました。結論からいうと無さそうです。
jdbcTemplateでClassを渡したら、いい感じにInsertしてくれるのないかな。
— きり丸 (@nainaistar) 2021年2月9日
テストクラスだから適当な扱いで良くて、今はMapでやってるけど項目数が30くらいあるから面倒なんだよなぁ・・・。
いろいろと探していたところ、新井 龍太郎@misty_rc様に、BeanPropertySqlParameterSource
を教えていただきました。
BeanPropertySqlParameterSourceとかそう言うことですかね?
— 新井 龍太郎 (@misty_rc) 2021年2月9日
今回の記事では、BeanPropertySqlParameterSource
を使用してSimpleJdbcInsert
に渡すMapを作成します。
なお、BeanPropertySqlParameterSource
とNamedParameterJdbcTemplate
を活用すればもっと簡単にInsertできるかもしれません。しかし、Hibernateの自動生成時に複合主キーの場合別のクラスにしており、解決に時間かかりそうだったので今回は使用していません。
環境
- Java
- 15
- H2
- 1.4.200
- org.springframework.boot:spring-boot-starter-web
- 2.4.0
- org.springframework.boot:spring-boot-starter-test
- 2.4.0
- org.mybatis.spring.boot:mybatis-spring-boot-starter-test
- 2.1.3
制約
- DBの項目はsnakeケース
- Javaのエンティティの項目はcamelケース
- エンティティは複合主キーだった場合、別のクラスが存在する
- エンティティにID以外のエンティティは存在しない
public class DummyIdDto implements Serializable { String idFirst; String idSecond; } public class DummyDto implements Serializable { DummyIdDto id; String fieldFirst; String fieldSecond; String fieldThird; // 基本テーブルの子の詳細テーブル // のような別テーブルのエンティティは無い // DummyDetailDto dummyDetail; }
ゴール
- エンティティを渡すとデータをInsertするメソッドを作成する
- メソッドの作成負荷を軽くする
- SimpleJdbcInsertに渡すMapを作成する
SimpleJdbcInsertの使い方は特に説明しません。
修正前のソースコード
@MybatisTest public class CommonSetup { @Autowired DataSource dataSource; SimpleJdbcInsert simpleJdbcInsert; protected void insertDummy(DummyDto... records) { this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource) .withTableName("DUMMY"); for (var record : records) { // TODO: ここのマッピングが非常にめんどくさい。 // このマッピングを何とかするのがこの記事の目的 HashMap<String, Object> parameter = new HashMap<>(); parameter.put("id_1", record.getId().getId1()); parameter.put("id_2", record.getId().getId2()); // ... parameter.put("field_1", record.getField1(); this.simpleJdbcInsert.execute(parameter); } } }
BeanPropertySqlParameterSourceのエンティティを生成する
エンティティを渡すだけで生成できます。
var source = new BeanPropertySqlParameterSource(entity)
エンティティの項目名のリストを取得する
次のメソッドのうち、どちらかを呼んで項目名のリストを取得する。現時点では、内部処理に差分はありませんでした。
getParameterNames(); getReadablePropertyNames();
項目名のリストをForで処理しつつ不要な変数を除外する
いくつか不要な変数もあるので、除外します。
少なくとも、classは除外してください。
for (String readablePropertyName : source.getReadablePropertyNames()) { if (List.of("class", "blank", "bytes", "empty").contains(readablePropertyName)) { continue; } }
項目名をDB用にスネークケースに変換する
jacksonライブラリを使用します。
Javaのキャメルケースのフィールド名を、DBの項目名に合わせてスネークケースに変換します。
new PropertyNamingStrategy
.SnakeCaseStrategy()
.translate(readablePropertyName)
DBに登録する値を取得する
DBに登録する値を取得します。
Object object = source.getValue(readablePropertyName);
取得した値が複合主キーのクラスの場合に再帰処理する
複合主キーだと識別できるようにしてください。
私はSerializable
をimplementsさせているので、instanceof
で識別させています。
複合主キーのエンティティを自メソッドの再帰処理に渡すことで、Mapが返却されます。
複合主キーのMapを、返却対象のMapにputAllで設定します。
if (object instanceof Serializable) { var primaryKeyMap = toMap((Serializable) object); rtn.putAll(primaryKeyMap); }
もし、子テーブルのエンティティが親エンティティにある場合は、キッチリ識別できるようにしてください。
今回の私の要件ではなかったので、検証していません。
返却対象のMapに更新対象の項目名と値を設定する
更新の項目名をMapに設定します。
rtn.put(snakeName, object);
修正後のソースコード
protected void insertDummy(DummyDto... records) { this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource) .withTableName("DUMMY") ; for (var record : records) { this.simpleJdbcInsert.execute(toMap(record)); } } public Map<String, Object> toMap(Serializable entity) { HashMap<String, Object> rtn = new HashMap<>(); var source = new BeanPropertySqlParameterSource(entity); for (String readablePropertyName : source.getReadablePropertyNames()) { if (List.of("class", "blank", "bytes", "empty").contains(readablePropertyName)) { continue; } var snakeName = new PropertyNamingStrategy.SnakeCaseStrategy().translate(readablePropertyName); Object object = source.getValue(readablePropertyName); if (object instanceof Serializable) { var primaryKeyMap = toMap((Serializable) object); rtn.putAll(primaryKeyMap); } rtn.put(snakeName, object); } return rtn; }
ソースコード
終わりに
正直、結局リフレクションしているのと大きくは変わらないので、もうちょっといい感じにできるとうれしいですね。
複合主キーが型として表現されているから、Hibernateの自動生成コードは好きでなのですが…。
ただ検証していないだけですので、実はマッピングできるかもしれないのですが。
こういう基本ライブラリ以外は自分でみつけるのは難しいので、もっと覚えていきたいです。
この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。