MyBatisでオブジェクトのマッピング時にセッターメソッドが本当に必要か調査する
はじめに
業務でMyBatis
を使っている。なんとなくチームの慣習で、マッピングするクラスにprivateのフィールドのセッターメソッドを用意するか、lombock
で@Data
や@Setter
のアノテーションをつけてセッターメソッドを用意して対応している。特に困っていることはないが、本当にそれでいいのだっけ?と気になったので、MyBatis
のソースコードを読んで、SELECT文の実行結果がどのようにクラスのフィールドにセットされるのか調べた。
TL;DR
- レコードをマッピングするクラスのフィールドがJavaで提供されている型(Stringとかintなど)で構成されている場合は、フィールドに値をセットするときはセッターがある時はセッターを使って値をセットし、セッターがない時はリフレクションを使ってフィールドに値を直接セットする。
- そのため、今回調査に使ったようなクラスにクエリの結果をマッピングしたい場合は、
MyBatis
のためだけにセッターメソッドを用意しているのであれば、セッターメソッドは不要。
調査方法
環境
- Java 17
- MyBatis 3.5.1
- MySQL 5.8
ソースコードの調査方法
- MySQLが提供しているサンプルデータベース
sakila
のactor
テーブルに対してSELECT文を実行し、Actorクラスを返すメソッド用意し、デバッグ実行やprint文使って挙動を調べた。
- MyBatisのソースコードを読んだときのメモ
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()
はMetaObject
のsetValue()
を読んでいて、さらに追いかけていくとBeanWrapper
のsetBeanProperty
を呼んでいる。さらにMetaClass
のgetSetInvoker()
が呼ばれ、これがセッターのメソッドかフィールドに直接値をセットする方法が返され、そのinvorkerを呼び出すことで値がセットされる。
余談(どのコンストラクタが優先して使われるのか?)
-
複数のコンストラクががあるとき、デフォルトのコンストラクタ(引数なしのコンストラクタ)があるときは、デフォルトのコンストラクタが呼ばれてインスタンスが初期化される。
-
デフォルトのコンストラクタがない時は、
@AutomapConstructor
がついているコンストラクタが呼ばれる。ない時は、レコードのカラムの数とコンストラクタの引数の数が一致し、型がレコードのカラムの型とコンストラクタの引数の型が一致するものが呼ばれる。
感想
- MyBatisが裏で色々やってくれていて、その恩恵に預かって開発できているのだなと改めて理解できた。
- 独自定義のクラスをフィールドにしたときや入れ子構造にしたときに、どのような挙動になるのかは今後調査したい。(おそらく、SQLを書くxmlで
resultMap
でどのクラスのどのフィールドに取得したレコードのカラムが対応するか書く必要があるあるので、その情報をもとにセッターメソッドを呼ぶか、リフレクションを使ってフィールドに値をセットしていそうではある。)
public class Hoge {
private Fuga fuga;
private List<Foo> fooList;
}
- ソースコードを読む方法の手段が一つ増えた(これまではInteliJの外部ライブラリで読みたいソースコードを見て、コードジャンプを使って読んでいく方法が主だったので)。いい読み方があれば知りたい。
Discussion