きり丸の技術日記

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

エンティティを渡すだけでDBのユニットテストのセットアップしたい・改

前にデータセットアップを面倒くさがって、BeanPropertySqlParameterSourceを使ってパラメータの項目名を取得していました。その時の記事。

そのまま使用していると、自分の目的通りには使えなかったので結局自作しました。

その時のメモ。

環境

  • 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以外のエンティティは存在しない
  • エンティティにはsetter/getter以外のメソッドが存在しうる
    • 前回の記事にはなかった新しい制約
public class DummyIdDto implements Serializable {
  String idFirst;
  String idSecond;
}

public class DummyDto implements Serializable {
  DummyIdDto id;
  String fieldFirst;
  String fieldSecond;
  String fieldThird;

  // 新しい制約
  public boolean isDummy() {
    return id == null;
  }
}

上手くいかなかった原因

BeanPropertySqlParamerSourceのプロパティ名を取得するロジックを理解していなかったから。


今でも完全理解はしていませんが、setter/getterを元にプロパティ名を構築しているようです。

ですので、私が用意した「isDummy」メソッドはbooleanのgetterメソッドとみなされ、プロパティ名を構築してしまいます。

この「isDummy」から「dummy」プロパティ名を推測してしまいました。

-- 実際にはDummyカラムは無いので、SQLエラーになる
INSERT INTO table_name (dummy) values (?);

解決策

2通りあります。

  1. isXXX, getYYY等のsetter/getterの命名規則に沿ったメソッドは作らない
  2. BeanPropertySqlParameterSourceを加工する

今回の対応は後者となります。

setter/getterの両方が揃っているプロパティ名を、意図するプロパティ名とすることにしました。

具体的には、BeanPropertySqlParameterSourceの処理をコピペし、プロパティ名を取得するメソッドのgetReadablePropertyNamesのみを加工しました。

PropertyDescriptorクラスのgetWriteMethodメソッドにsetter, getReadMethodメソッドにgetterが設定されています。そのどちらも設定されていない場合を、自作メソッドとして処理し、プロパティ名としては扱わないようにしました。

  private boolean isMethod(PropertyDescriptor pd) {
    return pd.getWriteMethod() == null || pd.getReadMethod() == null;
  }

BeanPropertySqlParameterSourceを加工した処理全体。

public class CustomBeanPropertySqlParameterSource {
  private final BeanWrapper beanWrapper;

  @Nullable
  private String[] propertyNames;


  /**
   * Create a new BeanPropertySqlParameterSource for the given bean.
   *
   * @param object the bean instance to wrap
   */
  public CustomBeanPropertySqlParameterSource(Object object) {
    this.beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
  }


  @Nullable
  public Object getValue(String paramName) throws IllegalArgumentException {
    try {
      return this.beanWrapper.getPropertyValue(paramName);
    } catch (NotReadablePropertyException ex) {
      throw new IllegalArgumentException(ex.getMessage());
    }
  }

  /**
   * Provide access to the property names of the wrapped bean.
   * Uses support provided in the {@link PropertyAccessor} interface.
   *
   * @return an array containing all the known property names
   */
  public String[] getReadablePropertyNames() {
    if (this.propertyNames == null) {
      List<String> names = new ArrayList<>();
      PropertyDescriptor[] props = this.beanWrapper.getPropertyDescriptors();
      for (PropertyDescriptor pd : props) {
        if (isMethod(pd)) {
          continue;
        }

        if (this.beanWrapper.isReadableProperty(pd.getName())) {
          names.add(pd.getName());
        }
      }
      this.propertyNames = StringUtils.toStringArray(names);
    }
    return this.propertyNames;
  }

  // 自作メソッド
  private boolean isMethod(PropertyDescriptor pd) {
    return pd.getWriteMethod() == null || pd.getReadMethod() == null;
  }

}

ソースコード

CustomBeanPropertySqlParameterSource.java github.com

テストコード:CommonSetupTests.java github.com

終わりに

よく分からない機能を、よくわからないまま使ったらだめですね。

まさか、メソッド名から逆引きしてプロパティ名を推測しているとは思いもしませんでした。

1つ、勉強になりました。


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

類似記事

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

f:id:nainaistar:20210320182415p:plain