欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

项目中一个查询列表突然无法查询到数据-Mybatis的懒加载问题

程序员文章站 2022-04-16 09:25:40
...

最近在做一个项目,前期运行一直良好,某次测试突然发现一个查询列表展示的小模块,突然就没有数据了,然后浏览器F12调试就会发现一堆的错误提示:

Failed to load resource: http://127.0.0.1:8090/XXX/static/lib/js/jquery-1.8.0.min.js 
the server responded with a status of 500 ()

莫名其妙,怎么就500错误了,后台数据库明明良好,然后对项目进行调试运行,也没发现异常,后台的controller正常执行完毕,并且list数据正确返回了,就是到前台页面就出现问题。

解决了一天也没头绪,一直以为是前端出问题了,第二天,代码跟踪到了mapper文件里面了,

咦?这有意思了,前人在mapper里面这样定义的:

<resultMap id="main" type="com.dtjx.model.systemset.Specialfield">
        <id column="id" property="id" />
        <result column="id" property="id" />
        <result column="itemvalue" property="itemvalue" />
        ...不重要字段省略...
        <association property="itemname" column="nameid" javaType="String" select="com.gph.saviorframework.dao.config.SubItemMapper.changeIdToValue"/>
        <association property="routesname" column="routes" javaType="String" select="com.dtjx.dao.route.RouteMapper.changeIdToValue"/>
        <association property="citysname" column="citys" javaType="String" select="com.dtjx.dao.route.RouteMapper.changeIdToValue"/>

        <collection property="showArr" ofType="com.dtjx.model.systemset.SpecialFieldText" >
         <result column="value" property="value"/>
         <result column="text" property="text"/>
        </collection>  
    </resultMap>

发现上面的关联查询,用了很多 association 和 collection

同时对Controller下断点跟踪会发现,后台查询数据库返回的数据列表:

项目中一个查询列表突然无法查询到数据-Mybatis的懒加载问题
项目中一个查询列表突然无法查询到数据-Mybatis的懒加载问题

发现这个封装的对象并不是我们自己定义的java bean ,很显然这个是被代理过的,所以现在很容易怀疑这个问题并不是前台的,而是后台的bug,研发的小姐姐估计也没想到吧,mybatis的resultMap并不是随处可用的,一个简单的列表只需要展现需要的字段即可,不要加载居多的其他信息。

解决方法肯定有了,另写一个查询list列表的resultMap就行了,把association 和 collection的关联去掉就能正常在前台也没展示了!

到这里并没有完

出现上面问题,我们分析更深层的原因,老铁们可以参考这篇博客:Mybatis的嵌套查询和延迟加载分析 ,真正的原因就是Mybatis的延迟加载,出现上面情况就是用了其中的javassist方法来实现延迟加载,导致前台无法解析json

实际上这个问题,广大网友早就友解决方法了,参考:mybitis懒加载Could not write JSON:No serializer…
并且有具体的报错信息,如下(奇怪,为啥我这里调试就没有异常信息呢?):

json:Could not write JSON: No serializer found for class org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory$EnhancedResultObjectProxyImpl and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS); nested exception is com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory$EnhancedResultObjectProxyImpl and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.xfishtour.entity.result2.JsonResult[\"data\"]->java.util.ArrayList[0]->com.xxx.xxxx_$$_jvstc2b_0[\"handler\"])

看到这异常提示,解决方法就更简单了,上面文章的老铁就给出了3中方法:

  • 关闭该查询的懒加载 fetchType=”eager”
<collection ...  fetchType="eager">
</collection>
  • 返回的类加上注解
@JsonIgnoreProperties(value = { "handler" })

直接将handler忽略掉,这个属于jackson中的内容,mybatis中使用jackson处理json数据

  • 配置json转换器属性SerializationFeature.FAIL_ON_EMPTY_BEANS为false。

这还没有完

革命尚未成功同志尚需努力啊!

后期在看这个Controller的查询,发现一个很搞笑的问题,传说中的“胡隆人”/或者恶意预留bug

这位程序小姐姐是这样写滴:

@RequestMapping(value = "/getlist")
@ResponseBody
public ModelMap getlist(
        @RequestParam(value = Constants.DEFAULT_CURRPAGE_MODEL_KEY, required = false, defaultValue = Constants.DEFAULT_PAGE_START) Integer start,
        @RequestParam(value = Constants.DEFAULT_PAGE_SIZE_MODEL_KEY, required = false, defaultValue = Constants.DEFAULT_PAGE_SIZE) Integer limit,
        Specialfield model) {
    ModelMap modelMap = new ModelMap();
    try {
        User userinfo = user();
        if (userinfo != null) {
            if (isNotEmpty(userinfo.getRoutes())) {
                List<String> trains = getRouteIds(userinfo);
                if (trains.size() > 0) {
                    model.setRouteIdAuthority(trains);
                }
            }
        // 分页的关键代码开始
        List<Specialfield> list = specialfieldService.get(model);
        int startmax = list.size() == 0 ? 0 : list.size() - 1;
        int fromIndex = Math.min((start - 1) * limit, startmax);
        int toIndex = Math.min(start * limit, list.size());
        modelMap.addAttribute(Constants.DEFAULT_RECORD_MODEL_KEY, list.subList(fromIndex, toIndex));
        modelMap.addAttribute(Constants.DEFAULT_COUNT_MODEL_KEY, list.size());
        // 分页结束
    } else {
            modelMap.addAttribute(Constants.DEFAULT_SUCCESS_KEY, Boolean.FALSE);
        }
    } catch (Exception e) {
        modelMap.addAttribute(Constants.DEFAULT_SUCCESS_KEY, Boolean.FALSE);
        modelMap.addAttribute(Constants.DEFAULT_ERROR_KEY, e.getMessage());
    }
    return modelMap;
}

看一下主要的分页代码

List<Specialfield> list = specialfieldService.get(model);
int startmax = list.size() == 0 ? 0 : list.size() - 1;
int fromIndex = Math.min((start - 1) * limit, startmax);
int toIndex = Math.min(start * limit, list.size());
modelMap.addAttribute(Constants.DEFAULT_RECORD_MODEL_KEY, list.subList(fromIndex, toIndex));
modelMap.addAttribute(Constants.DEFAULT_COUNT_MODEL_KEY, list.size());
  • 第一行,查询数据
  • 第二行,计算数据总数
  • 第三行,从第一个开始
  • 第四和,从哪里结束
  • 第五行,分页取数据,并放给前台
  • 第六行,给前台总数

这个分页大概是可以吧?不过并不好,并且是非常不好,查询个三百五百条的没有问题,当时设想一下,这个表里面有十万条数据咋办?

每次都获取全部,那还用分页干啥啊?

我接收后,看到上面的代码,随手就改了,如下:

PageHelper.startPage(start, limit);
List<Map<String, Object>> list = specialfieldService.getlist(model);
modelMap.addAttribute(Constants.DEFAULT_RECORD_MODEL_KEY, list);
modelMap.addAttribute(Constants.DEFAULT_COUNT_MODEL_KEY, ((Page<Map<String, Object>>) list).getTotal());
modelMap.addAttribute(Constants.DEFAULT_SUCCESS_KEY, Boolean.TRUE);

很easy,是不是,用的PageHelper插件去做的分页操作,也跟项目中框架用的统一的分页一致,为啥还要用list的截取分页呢?

当时没考虑,后来才发现小姐姐其实也是无奈呢~~~

PageHelper插件跟Mybatis中的collection一对多关联查询分页总是出现问题,莫名奇妙,数据分页总是不对!

项目中用到了pageHelper,但是在mybatis的resultmap中使用了collection去封装一对多的映射关系。

分页查询后始终记录数和实际数据对不上,原因出在pageHelper和collection有冲突。

原因: pagehelper是基于sql查询结果的数据条数进行拦截的,但是collection会把相同结果映射为List,

所以执行顺序是先拦截条数,后封装结果,造成了数据分页后查询数量变少。

看看原先mapper的xml写法,如下:

<resultMap id="main" type="com.dtjx.model.systemset.Specialfield">
    <id column="id" property="id" />
    <result column="id" property="id" />
    ....
    <collection property="showArr" ofType="com.dtjx.model.systemset.SpecialFieldText" fetchType="lazy">
     <result column="value" property="value"/>
     <result column="text" property="text"/>
    </collection>
</resultMap>

<select id="getlist" parameterType="com.dtjx.model.systemset.Specialfield" resultMap="main">
    SELECT
        a.id,
        ...,
        d.text AS text,
        d.value AS value
    FROM
        dtjx_specialfield a
    LEFT JOIN dtjx_transmissioncode b ON a.codeid = b.id
    LEFT JOIN sf_sub_item c ON a.nameid = c.SUB_ITEM_ID
    LEFT JOIN dtjx_special_field_text d ON a.id = d.specialFieldId
    WHERE
        1 = 1
</select>

上面的查询会直接把有相同id下的text和value封装成list,假设分页是每页10条,那么就会如果list超过10条,也只能被pagehelper取出10条记录,并且返回给页面的也只有一条记录;

那么怎么解决呢?

这种情况下,不应该使用上面的写法,应该改成使用关联查询,如下:

<resultMap id="main" type="com.dtjx.model.systemset.Specialfield">
    <id column="id" property="id" />
    <result column="id" property="id" />
    ....
<collection property="showArr" ofType="com.dtjx.model.systemset.SpecialFieldText" column="id"
            fetchType="lazy" select="com.dtjx.dao.systemset.SpecialfieldMapper.showArrById">
        </collection>
</resultMap>
<!--增加关联查询 -->
 <select id="showArrById" parameterType="string" resultType="com.dtjx.model.systemset.SpecialFieldText">
    select value, text from dtjx_special_field_text where specialFieldId = #{id}
</select>

<select id="getlist" parameterType="com.dtjx.model.systemset.Specialfield" resultMap="main">
    SELECT
        a.id,
        ...,<!-- 去掉相关的 text和value,不要left join 有关text和value的表
        d.text AS text,
        d.value AS value -->
    FROM
        dtjx_specialfield a
    LEFT JOIN dtjx_transmissioncode b ON a.codeid = b.id
    LEFT JOIN sf_sub_item c ON a.nameid = c.SUB_ITEM_ID
    WHERE
        1 = 1
</select>

一定要让主查询去做分页操作,其他的作为关联查询,这就OK啦!