问题复现:
我有两张表,1张是学生表,另一张是家属表。学生与家长是一对多关系。现在有个需求,查出每个学生和他的家属的姓名,并分页展示。关联关系是,学生fa_id作为学生表的主键,家长表内通过学生fa_id与该学生关联。
查询方式是:
学生表fb left join 家长表 fp on fb.fa_id = fp.fa_id
然后由mybatis根据嵌套了collection的resultMap来将数据进行封装。(问题就出在嵌套的resultMap上)
传输对象如下:
public class UserBasicInfoTo {
private String faId; //学生id
private String fbName; // 学生名
private String fbPhone; //学生电话号码
private List<String> agedPeople; //学生家属名列表
public UserBasicInfoTo() {
}
//getters and setters
}
xml配置如下:
<resultMap id="UserInfoToMap" type="com.cloud.fmmp.base.bean.to.UserBasicInfoTo">
<result column="FB_Name" property="fbName"></result>
<result column="FA_ID" property="faId"></result>
<result column="FB_Phone" property="fbPhone"></result>
<collection property="agedPeople" ofType="java.lang.String">
<result column="pb_cnname"></result>
</collection>
</resultMap>
<select id="getUserInfoTos" resultMap="UserInfoToMap">
SELECT FB_Name, FB_Phone, fb.FA_ID, PB_CNName FROM ins_family_base fb
LEFT JOIN ins_family_people fp
ON fb.FA_ID = fp.FA_ID
WHERE fb.SYS_ID = #{sysId}
<if test="fbName != null">
and fb.FB_Name = #{fbName}
</if>
<if test="fbPhone != null">
and fb.FB_Phone = #{fbPhone}
</if>
</select>
对应的mapper接口
@Repository
public interface InsFamilyBaseMapper extends BaseMapper<InsFamilyBase> {
List<UserBasicInfoTo> getUserInfoTos(@Param("sysId") String sysId, @Param("fbName") String fbName, @Param("fbPhone") String fbPhone);
}
service代码片段
Integer pageNum = Integer.parseInt(map.get("pageNum"));
Integer pageSize = Integer.parseInt(map.get("pageSize"));
PageHelper.startPage(pageNum, pageSize);
// join两张表,UserBasicInfoTo嵌套一个List
List<UserBasicInfoTo> tos = insFamilyBaseMapper.getUserInfoTos(sysId, fbName, fbPhone);
PageInfo<UserBasicInfoTo> pageInfo = new PageInfo<>(tos);
return pageInfo;
上述代码在返回pageInfo时,结果的总条数不对。查出来的对象个数为13,但是pageInfo却显示有19条记录,如下所示:
这里明显是不对的,但哪里出问题了呢?
在讲resultSetHandler的时候,明确提到过,它对有嵌套的resultMap的处理时,做了一次ensureNoRowBounds的检查,即确保他自己没有分页参数。
也就是说,在mybatis的resultSetHandler处理原始带有嵌套的resultMap时,是没有带分页参数的。那么这个时候pageHelper是怎么工作的呢?
我们实现一个interceptor拦截打印一下执行的sql,就了然了。
1.把pageHelper注释掉
sql查到了19条记录,此时并没有封装成UserBasicInfoTo,即数据库中有19条记录是满足查询条件的。
然后resultSetHandler对结果集进行处理封装,对相同fa_id的结果集进行合并处理(相同fa_id的结果集都封装成1个infoTO对象,对嵌套的resultMap处理并放入到该对象的List<String> agedPeople中。)所以处理完成之后就剩13个对象了.如图
现在明白了,19是原始的记录数,13是resultSetHandler封装后的结果数。
那么为什么pageHelper能得到总的记录数呢?
原因是他做了这样一个操作:拦截了当前执行的sql语句,并通过select count(0)语句得到了符合查询条件的记录数,然后将其作为总页数,即19。
然后,对sql语句进行拼接,加上分页参数。
但是mybatis在处理statement的时候,将分页参数提取出来了,存放在了rowBounds中,向数据库发送的query中并没有limit a, b ,而是最后在resultSetHandler处理结果集的时候,跳过了分页参数影响的那些行。
[图片上传中...(image.png-f4c9f3-1616516429638-0)]
所以,pageHelper对嵌套的resultMap进行分页处理的时候,还有可能会导致嵌套的数据不完整,如Vo中应当包含一个List,该list中如果有数据被分页参数跳过了,那么list内的数据就不完整了。
最后举几个例子来说明pageHelper在处理嵌套的resultMap时,出现上面所说的数据总数和数据不完整的问题。
- pageNum =3, pageSize = 5, 转换成sql则是limit 10, 5获取第11-15条记录,假设这里面有3个不同的学生,5个家长。如果这三个学生还有其他满足条件的记录,但这些记录不在第11-15条之间,那么家长信息就不完整了。另外,pageinfo的total是上面所说的select count(0) from xxx where (columns subject to condition),也就是总的记录数,但我现在要的是不同的学生数,所以page中的count也不对了。
-
举个极端点的例子来说,pageNum = 2,时,在pageSize = 1和pageSize = 20的两种条件下,查询出来的agedPeople截然不同。
看出问题来了吗? 现在总记录数是19条,当pageSize = 20的时候,该学生所有的家长都包含在了agedPeople中了。但是pageSize= 1的时候,仅仅包含了第2条记录对应的家长。
-
总结一下问题就是:我告诉你我要查第i到j个学生的信息,你却给了我数据库第i到j条记录封装成的学生信息。我想要知道我一共有多少个学生符合查询条件,你却告诉我一共有多少条记录满足查询条件。这完全是两码事嘛。
问题的解决办法:避免在使用分页插件的时候用嵌套的resultMap。 如resultMap中去嵌套1个List。
目前可以解决的办法有两种:
- 1.使用子查询:
<resultMap id="UserBasicInfoToMap" type="com.cloud.fmmp.base.bean.to.UserBasicInfoTo">
<result column="FB_Name" property="fbName"></result>
<result column="FA_ID" property="faId"></result>
<result column="FA_NickName" property="faNickname"></result>
<result column="FB_Phone" property="fbPhone"></result>
<!--注意看这里,里面包含一个select子查询-->
<collection property="agedPeople" javaType="java.util.List" ofType="java.lang.String" column="fa_id = fa_id" select="selectAgedPeople"></collection>
</resultMap>
<select id="getUserBasicInfoTos" resultMap="UserBasicInfoToMap">
SELECT FB_Name, FB_Phone, fb.FA_ID, fa.FA_NickName FROM ins_family_base fb
LEFT JOIN ins_family_account fa
ON fb.FA_ID = fa.FA_ID
WHERE fb.SYS_ID = #{sysId}
<if test="fbName != null">
and fb.FB_Name = #{fbName}
</if>
<if test="fbPhone != null">
and fb.FB_Phone = #{fbPhone}
</if>
</select>
<select id="selectAgedPeople" resultType="java.lang.String">
select PB_CNName from ins_family_people where fa_id = #{fa_id}
</select>
mapper接口方法、封装数据使用的javaBean也相同。唯一不同的就是select和resultMap了(见上)。
List<UserBasicInfoTo> getUserBasicInfoTos(@Param("sysId") String sysId, @Param("fbName") String fbName, @Param("fbPhone") String fbPhone);
此时pageNum=2, pageSize =1的结果:由于对学生进行了过滤,共有两个符合条件的学生,当前页显示其中一个人的数据,学生的家长数据完整。
- 2.分步查询, 先将包含rowKey的主体信息查出来,即先去主表查出能区分不同实体的属性。然后根据主体属性与嵌套的属性关系,查嵌套属性并自己在java代码内完成封装。这种方式很容易理解,就不罗列代码了。