きり丸の技術日記

技術検証したり、資格等をここに残していきます。

MyBatisでEagerLoad, LazyLoadをする(Annotationにて)

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にはパッケージ名を含めたメソッド名を定義します。fetchTypeEAGERLAZYを定義することで、取得方法を変更できます。

最後に、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の挙動確認

挙動を確認するには、ログレベルをDEBUGTRACEにすると分かりやすいログが出てきます。

logging:
  level:
    # 今回用意したパッケージ名
     kirimaru.biz.mapper: DEBUG

FetchTypeEAGERにしている場合、祖先クラス取得時に子孫までデータ取得するのが==>=の数で分かります。

  • 企業取得時
    • ==>
  • 組織取得時
    • ====>
  • 人員取得時
    • ======>
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

FetchTypeLAZYにしている場合、必要なタイミングでデータロードを行っています。今回の場合、テストにて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を使用していきたいと考えているので、積極的にこのような機能は確認していきたいです。

参考情報