🗺️

MyBatisでオブジェクトのマッピング時にセッターメソッドが本当に必要か調査する

2023/05/22に公開

はじめに

業務でMyBatisを使っている。なんとなくチームの慣習で、マッピングするクラスにprivateのフィールドのセッターメソッドを用意するか、lombock@Data@Setterのアノテーションをつけてセッターメソッドを用意して対応している。特に困っていることはないが、本当にそれでいいのだっけ?と気になったので、MyBatisのソースコードを読んで、SELECT文の実行結果がどのようにクラスのフィールドにセットされるのか調べた。

TL;DR

  • レコードをマッピングするクラスのフィールドがJavaで提供されている型(Stringとかintなど)で構成されている場合は、フィールドに値をセットするときはセッターがある時はセッターを使って値をセットし、セッターがない時はリフレクションを使ってフィールドに値を直接セットする。
  • そのため、今回調査に使ったようなクラスにクエリの結果をマッピングしたい場合は、MyBatisのためだけにセッターメソッドを用意しているのであれば、セッターメソッドは不要。

調査方法

環境

  • Java 17
  • MyBatis 3.5.1
  • MySQL 5.8

ソースコードの調査方法

Actor.java
public class Actor {
  private int actorId;
  private String firstName;
  private String lastName;

  public Actor(int actorId, String firstName, String lastName) {
    System.out.println("引数ありのコンストラクタが呼ばれた " + actorId + ", " + firstName + ", " + lastName);
    this.actorId = actorId;
    this.firstName = firstName;
    this.lastName = lastName;
  }

  public Actor() {
    System.out.println("引数なしのコンストラクタが呼ばれた");
  }

  public int getActorId() {
    return actorId;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    System.out.println("setFirstNameが呼ばれた " + firstName);
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    System.out.println("setLastNameが呼ばれた " + lastName);
    this.lastName = lastName;
  }

  @Override
  public String toString() {
    return firstName + " " + lastName + " (id: " + actorId + ")";
  }
}
ActorMapper.java
public interface ActorMapper {
  Actor selectActor(int id);
}
ActorMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.sample.mapper.ActorMapper">
  <select id="selectActor" resultType="org.apache.ibatis.sample.entity.Actor">
    SELECT
      actor_id,
      last_name,
      first_name
    FROM actor
    WHERE actor_id = #{id}
  </select>
</mapper>

ソースコードの調査

概要

SQLのクエリを実行するハンドラやクエリの実行結果をオブジェクトにマッピングするハンドラなどがあり、クエリの結果をオブジェクトにマッピングする処理はDefaultResultSetHandle.javaで行われていた。ざっくりいうと、インスタンスを生成し、フィールドに値をセットする処理がapplyAutomaticMappings()で行われている。

フィールドに値をセットする方法は①setterメソッドを動的に実行する方法と②フィールドに値を直接セットする方法があり、どちらの方法で値をセットするかどうかは、Reflectorクラスにある情報をもとに判断される。

Reflectorクラスはレコードをマッピングしたいクラスのメタ情報(フィールドやgetterやsetterやデフォルトコンストラクタ)をフィールに保持するクラスである。フィールドの一つにセッターの情報を保持するsetMethodsというフィールドがある。このインスタンスを生成するときに、addFields()が呼ばれるが、addFields()が呼ばれる前に実行されるaddSetMethods()でセッターメソッドが見つからない時はリフレクションでフィールドに値をセットするinvorkerをsetMethodsにセットする。よって、セッターメソッドがクラスになくてもフィールドに直接値をセットする方法でフィールドに値をセットできるので、セッターメソッドが必ずしも必要でないことが分かった。

メモ

メモ

DefaultResultSetHandle.javaで値をセットする処理はapplyAutomaticMappings()で行われている。applyAutomaticMappings()MetaObjectsetValue()を読んでいて、さらに追いかけていくとBeanWrappersetBeanPropertyを呼んでいる。さらにMetaClassgetSetInvoker()が呼ばれ、これがセッターのメソッドかフィールドに直接値をセットする方法が返され、そのinvorkerを呼び出すことで値がセットされる。

余談(どのコンストラクタが優先して使われるのか?)

  • 複数のコンストラクががあるとき、デフォルトのコンストラクタ(引数なしのコンストラクタ)があるときは、デフォルトのコンストラクタが呼ばれてインスタンスが初期化される。

  • デフォルトのコンストラクタがない時は、@AutomapConstructorがついているコンストラクタが呼ばれる。ない時は、レコードのカラムの数とコンストラクタの引数の数が一致し、型がレコードのカラムの型とコンストラクタの引数の型が一致するものが呼ばれる。

感想

  • MyBatisが裏で色々やってくれていて、その恩恵に預かって開発できているのだなと改めて理解できた。
  • 独自定義のクラスをフィールドにしたときや入れ子構造にしたときに、どのような挙動になるのかは今後調査したい。(おそらく、SQLを書くxmlでresultMapでどのクラスのどのフィールドに取得したレコードのカラムが対応するか書く必要があるあるので、その情報をもとにセッターメソッドを呼ぶか、リフレクションを使ってフィールドに値をセットしていそうではある。)
public class Hoge {
    private Fuga fuga;
    private List<Foo> fooList;
}
  • ソースコードを読む方法の手段が一つ増えた(これまではInteliJの外部ライブラリで読みたいソースコードを見て、コードジャンプを使って読んでいく方法が主だったので)。いい読み方があれば知りたい。

参考

Discussion