MyBatisのアノテーションで親クラスのデータ取得時に、子や孫クラスを一緒に取得する方法(EAGER LOAD)、必要になってから取得する方法(LAZY LOAD)を記載します。
なお、常に子クラスと一緒に扱うときはEAGER LOAD
, 必要に応じて子クラスを使用する場合はLAZY LOAD
をよく使用します。
環境
- Java
- 17
- org.springframework.boot:spring-boot-starter-jdbc
- 2.6.4
- org.mybatis.spring.boot:mybatis-spring-boot-starter
- 2.2.0
前提
- 論理的には3階層、物理的には5階層を祖先から子孫まで取得します
次のテーブル構成で、祖先テーブルの企業をRDBから取得する際に、子孫テーブルの人員まで取得できるようにします。
- 企業
- 組織
- 人員
- 企業と組織を紐づけるテーブル
- 組織と人員を紐づけるテーブル
また、企業には子テーブルの組織リスト、組織には子テーブルの人員リストをもっています。
public class CompanyDto implements Serializable { String companyId; String name; List<DepartmentDto> departmentList; }
対応
@Results
でデータ取得した後のマッピングを定義します。今回は、企業
の子テーブルの組織
も取得したいので、@Result
で組織リストのdepartmentList
にマッピングするように定義します。
property
にマッピングしたい変数名を記載します。
今回は複数件紐づくためmany
を使用しています。1件ではone
を使用してください。select
にはパッケージ名を含めたメソッド名を定義します。fetchType
にEAGER
かLAZY
を定義することで、取得方法を変更できます。
最後に、column
に上で定義したselect
のメソッドに渡すパラメータを記載します。
@Results(id = "company", value = { @Result( property = "departmentList", many = @Many(select = "kirimaru.biz.mapper.DepartmentMapper.findByCompanyId", fetchType = FetchType.EAGER), column = "company_id" ) } ) @Select(""" SELECT * FROM COMPANY WHERE COMPANY_ID = #{companyId} """) CompanyDto findByPrimaryKey(@Param("companyId") String id);
組織Mapper側にselect
で定義したメソッドを用意します。
@Select(""" SELECT DEPARTMENT.* FROM DEPARTMENT, COMPANY_DEPARTMENT WHERE DEPARTMENT.DEPARTMENT_ID = COMPANY_DEPARTMENT.DEPARTMENT_ID AND COMPANY_ID = #{companyId} """) List<DepartmentDto> findByCompanyId(@Param("companyId") String id);
後は同じように組織Mapperの@Results
を定義し、人員Mapperを定義すると祖先の企業Mapperにて、子孫の人員を取得できます。
備考
EAGER LOAD, LAZY LOADの挙動確認
挙動を確認するには、ログレベルをDEBUG
やTRACE
にすると分かりやすいログが出てきます。
logging: level: # 今回用意したパッケージ名 kirimaru.biz.mapper: DEBUG
FetchType
をEAGER
にしている場合、祖先クラス取得時に子孫までデータ取得するのが==>
の=
の数で分かります。
- 企業取得時
- ==>
- 組織取得時
- ====>
- 人員取得時
- ======>
findByPrimaryKey : ==> Preparing: SELECT * FROM COMPANY WHERE COMPANY_ID = ? findByPrimaryKey : ==> Parameters: 1(String) findByCompanyId : ====> Preparing: SELECT DEPARTMENT.* FROM DEPARTMENT, COMPANY_DEPARTMENT WHERE DEPARTMENT.DEPARTMENT_ID = COMPANY_DEPARTMENT.DEPARTMENT_ID AND COMPANY_ID = ? findByCompanyId : ====> Parameters: 1(String) findByDepartmentId : ======> Preparing: SELECT USERS.* FROM DEPARTMENT_MEMBER, USERS WHERE DEPARTMENT_MEMBER.USER_ID = USERS.USER_ID AND DEPARTMENT_ID = ? findByDepartmentId : ======> Parameters: 100(String) findByDepartmentId : <====== Total: 2 findByDepartmentId : ======> Preparing: SELECT USERS.* FROM DEPARTMENT_MEMBER, USERS WHERE DEPARTMENT_MEMBER.USER_ID = USERS.USER_ID AND DEPARTMENT_ID = ? findByDepartmentId : ======> Parameters: 101(String) findByDepartmentId : <====== Total: 1 findByDepartmentId : ======> Preparing: SELECT USERS.* FROM DEPARTMENT_MEMBER, USERS WHERE DEPARTMENT_MEMBER.USER_ID = USERS.USER_ID AND DEPARTMENT_ID = ? findByDepartmentId : ======> Parameters: 102(String) findByDepartmentId : <====== Total: 0 findByCompanyId : <==== Total: 3 findByPrimaryKey : <== Total: 1
FetchType
をLAZY
にしている場合、必要なタイミングでデータロードを行っています。今回の場合、テストにてassertThat
でクラス比較を行ったタイミングで一斉にデータロードが走っています。
findByPrimaryKey : ==> Preparing: SELECT * FROM COMPANY WHERE COMPANY_ID = ? findByPrimaryKey : ==> Parameters: 1(String) findByPrimaryKey : <== Total: 1 findByCompanyId : ==> Preparing: SELECT DEPARTMENT.* FROM DEPARTMENT, COMPANY_DEPARTMENT WHERE DEPARTMENT.DEPARTMENT_ID = COMPANY_DEPARTMENT.DEPARTMENT_ID AND COMPANY_ID = ? findByCompanyId : ==> Parameters: 1(String) findByCompanyId : <== Total: 3 findByDepartmentId : ==> Preparing: SELECT USERS.* FROM DEPARTMENT_MEMBER, USERS WHERE DEPARTMENT_MEMBER.USER_ID = USERS.USER_ID AND DEPARTMENT_ID = ? findByDepartmentId : ==> Parameters: 100(String) findByDepartmentId : <== Total: 2 findByDepartmentId : ==> Preparing: SELECT USERS.* FROM DEPARTMENT_MEMBER, USERS WHERE DEPARTMENT_MEMBER.USER_ID = USERS.USER_ID AND DEPARTMENT_ID = ? findByDepartmentId : ==> Parameters: 101(String) findByDepartmentId : <== Total: 1 findByDepartmentId : ==> Preparing: SELECT USERS.* FROM DEPARTMENT_MEMBER, USERS WHERE DEPARTMENT_MEMBER.USER_ID = USERS.USER_ID AND DEPARTMENT_ID = ? findByDepartmentId : ==> Parameters: 102(String) findByDepartmentId : <== Total: 0
IDEサポートが得られない
select
で対象のメソッドを文字列でマッピングするので、IDEではマッピングされません。メソッド名をリネームしたり、不要メソッドだと思って削除すると動かなくなるので、注意です。
データマッピング用の何かが付与される
※ 再現ができていないので、記憶違いかも。
基本的には意識不要ですが、リフレクション等々でデータを取得しようとするときに、余計なデータが取れることがあります。都度都度確認すればよいのですが、余計な挙動を引き起こしかねないのでリフレクションを使う時には動作確認が必要です。
ソースコード
他の細かいところは同じハッシュのソースコードを読んでください。
終わりに
個人的にはN+1を簡単に誘発するので、どちらのやり方も好きではありません。ただ、うまくいけばソースコードの記述量が減りますし、データアクセス数が減って最適化もしてくれているようなので高速化も見込めて便利です。
基本的に、MyBatisの公式ヘルプはXMLでの説明が多いです。 アノテーションの説明もありますが、XMLでの説明ほどありませんので、この記事を書きました。公式ヘルプ的には、こっちの書き方の方が現代みたいなのですが…。
とりあえず、個人的には関わるプロジェクトは今後もMyBatis
を使用していきたいと考えているので、積極的にこのような機能は確認していきたいです。