きり丸の技術日記

技術・エンジニアのイベント・資格等はこちらにまとめる予定です

エンティティを渡すだけでDBのユニットテストのセットアップしたい(SimpleJdbcInsertとBeanPropertySqlParameterSource)

皆さんはDBのユニットテストを行うとき、どのようにテストのセットアップを行っていますか。

私はSimpleJdbcInsertを使用しています。テーブル名とInsert対象のカラムと値のマップを渡すだけで更新してくれるので、個人的には使いやすいと思っています。

しかし、Insert対象のカラムが30-40項目あるテーブルのマップを20個分作成すると、非生産的な上に手間がかかります。

Selectするときは、Responseのクラスを渡してあげるとマッピングしてくれるので、Insertする際も同様の機能があるはずと思っていました。結論からいうと無さそうです。

いろいろと探していたところ、新井 龍太郎@misty_rc様に、BeanPropertySqlParameterSourceを教えていただきました。

今回の記事では、BeanPropertySqlParameterSourceを使用してSimpleJdbcInsertに渡すMapを作成します。


なお、BeanPropertySqlParameterSourceNamedParameterJdbcTemplateを活用すればもっと簡単に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;
  }

ソースコード

github.com

終わりに

正直、結局リフレクションしているのと大きくは変わらないので、もうちょっといい感じにできるとうれしいですね。

複合主キーが型として表現されているから、Hibernateの自動生成コードは好きでなのですが…。

ただ検証していないだけですので、実はマッピングできるかもしれないのですが。

こういう基本ライブラリ以外は自分でみつけるのは難しいので、もっと覚えていきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

関連記事

nainaistar.hatenablog.com

f:id:nainaistar:20201013111905p:plain