MyBatis Plusでカスタムマッパーと複数テーブル結合を組み合わせたページネーション時にデータ不足が発生する問題

問題の概要

最近のプロジェクトでMyBatis Plusを使用しており、ページネーションプラグインも同社製のものを採用しています。業務要件では顧客リストを取得し、それぞれの顧客リストにサブリストを含む構造が必要ですが、ページネーションプラグインを使用したクエリ実行後に合計件数がサブリストの件数となり、顧客リストとサブリストの対応がずれてしまう現象が発生しています。

1. MyBatis Plusプラグインの設定

@Configuration
@MapperScan("com.guigu.mapper") // ランチクラスの注釈をここに移動することも可能
public class MyBatisPlusConfig {
    /**
     * 
     * 1. MyBatis Plusのプラグインをどのように設定するか?
     *    必要なタイプはMyBatisPlusInterceptorであり、これはMyBatis Plusのインターセプターで、プラグインの設定に使用される。
     * 2. なぜMyBatisPlusInterceptorインターセプターを使用するのか?
     *    内部ではMyBatisのページネーションプラグインと同じ仕組みで動作し、処理フローは以下の通り:
     *    (1)最初にクエリを実行
     *    (2)インターセプターがクエリをインターセプト
     *    (3)インターセプターがクエリに追加処理を行い、ページネーションを実現
     * 3. MyBatisのプラグイン設定と比較?
     *    同様にインターセプター方式で設定している
     *
     * @return MyBatisPlusInterceptor
     */
    @Bean
    public MyBatisPlusInterceptor myBatisPlusInterceptor() {
        MyBatisPlusInterceptor interceptor = new MyBatisPlusInterceptor();
        // ページネーションプラグインの追加
        // パラメータ:new PaginationInnerInterceptor(DbType.MYSQL) はMySQL専用の内部ページネーションプラグイン
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

2. Mapper文の記述

 需求:年齢でユーザー一覧を取得し、ページネーション表示
     *
     * 第一歩:XMLでカスタムページネーション、Mapperインターフェースメソッド
     *       第1歩:MyBatis PlusのページネーションプラグインがカスタムSQLに作用するためには、
     * 		最初のパラメータはページオブジェクトでなければならない:@Param("page") Page<User> page。
     * 第二歩:Mapperインターフェースメソッドが2つのパラメータを持つ場合
     *       方案1:MyBatisが提供するアクセス方法
     *       方案2:@paramを使用して名前付きパラメータを設定し、パラメータのアクセスルールを規定する
    Page<CustomerEntity> queryCustomerList(@Param("page") Page<CustomerEntity> customerPage,
                                           @Param("customerName") String customerName,
                                           @Param("deptIds") List<Long> deptIds,
                                           @Param("userIds") List<Long> userIds,
                                           @Param("delFlag") Integer logicNotDeleteValue)
    <resultMap id="customerList" type="customerEntity">
        <id property="customerId" column="customer_id"/>
        <result property="customerName" column="customer_name"/>
        <collection property="children" select="queryList" column="customer_id" ofType="customerInfoEntity">
        </collection>
        <collection property="children" ofType="customerInfoEntity">
            <id property="customerInfoId" column="customer_info_id"/>
            <result property="customerId" column="customer_id"/>
            <result property="deptId" column="dept_id"/>
            <result property="industry" column="industry"/>
            <result property="level" column="level"/>
            <result property="source" column="source"/>
            <result property="highSeas" column="high_seas"/>
            <result property="remark" column="remark"/>
            <result property="crtTime" column="crt_time"/>
            <result property="crtName" column="crt_name"/>
            <result property="crtUserId" column="crt_user_id"/>
        </collection>
    </resultMap>
    <select id="queryCustomerList" resultMap="customerList">
        SELECT
        c.customer_id,
        c.customer_name,
        ci.customer_info_id,
        ci.customer_id,
        ci.dept_id,
        ci.industry,
        ci.level,
        ci.source,
        ci.high_seas,
        ci.remark,
        ci.crt_time,
        ci.crt_name,
        ci.crt_user_id
        FROM customer c
        JOIN customer_info ci on c.customer_id = ci.customer_id
        where ci.high_seas = 0
        and ci.del_flag = #{delFlag}
        and c.del_flag =#{delFlag}
        <if test="customerName != null">
            AND c.customer_name like concat('%',#{customerName},'%')
        </if>
        <if test="deptIds.size>0">
            AND ci.dept_id in
            <foreach collection="deptIds" item="id" separator="," open="(" close=")">
                #{id}
            </foreach>
        </if>
        <if test="userIds.size>0">
            AND ci.crt_user_id in
            <foreach collection="userIds" item="id" separator="," open="(" close=")">
                #{id}
            </foreach>
        </if>
    </select>

上記のクエリ結果はcustomer_infoテーブルの件数となり、customerテーブルの件数ではなく、顧客リストが複数のサブリストを持つ場合、同じ顧客が複数表示される。

解決策

正しい1対多のページネーション結果を取得するには、MyBatisが提供するサブクエリマッピングを使用する方法があります。

	<resultMap id="customerList" type="customerEntity">
        <id property="customerId" column="customer_id"/>
        <result property="customerName" column="customer_name"/>
        <collection property="children" select="queryList" column="customer_id" ofType="customerInfoEntity">
        </collection>
    </resultMap>

    <select id="queryList" resultType="customerInfoEntity">
        select ci.customer_info_id,
               ci.customer_id,
               ci.dept_id,
               ci.industry,
               ci.level,
               ci.source,
               ci.high_seas,
               ci.remark,
               ci.crt_time,
               ci.crt_name,
               ci.crt_user_id
        from customer_info ci
        where ci.customer_id = #{customer_id};
    </select>

    <!--    公海池に未加入の顧客一覧を取得-->
    <select id="queryCustomerList" resultMap="customerList">
        SELECT
        c.customer_id,
        c.customer_name,
        ci.customer_info_id,
        ci.customer_id,
        ci.dept_id,
        ci.industry,
        ci.level,
        ci.source,
        ci.high_seas,
        ci.remark,
        ci.crt_time,
        ci.crt_name,
        ci.crt_user_id
        FROM customer c
        JOIN customer_info ci on c.customer_id = ci.customer_id
        where ci.high_seas = 0
        and ci.del_flag = #{delFlag}
        and c.del_flag =#{delFlag}
        <if test="customerName != null">
            AND c.customer_name like concat('%',#{customerName},'%')
        </if>
        <if test="deptIds.size>0">
            AND ci.dept_id in
            <foreach collection="deptIds" item="id" separator="," open="(" close=")">
                #{id}
            </foreach>
        </if>
        <if test="userIds.size>0">
            AND ci.crt_user_id in
            <foreach collection="userIds" item="id" separator="," open="(" close=")">
                #{id}
            </foreach>
        </if>
    </select>

注意: collection内のcolumn属性には二つのテーブルを関連付けるフィールド(つまりcustomer_id)を記入する必要があります。

タグ: MyBatis Plus ページネーションプラグイン 複数テーブル結合 サブクエリマッピング

7月10日 22:56 投稿