项目中一个查询列表突然无法查询到数据-Mybatis的懒加载问题
最近在做一个项目,前期运行一直良好,某次测试突然发现一个查询列表展示的小模块,突然就没有数据了,然后浏览器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下断点跟踪会发现,后台查询数据库返回的数据列表:
发现这个封装的对象并不是我们自己定义的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啦!