问题:使用mybatis-plus进行列表分页查询数据时(多表联查),如果查询条件是子表的时候,或者显示的字段是子表的字段,我们应该怎么办?
一、我们先看看常规的查询是怎么实现的,这里的表关系是:一个user表对应多个car表(car的pkUser字段与user的id关联)
1.单表的分页查询(string是前端传的条件,然后我们动态的拼接wrapper。)
注:这里应该是遍历json数据,然后才动态拼接wrapper,这里就略过了
2.多表的分页查询(先查主表然后再查子表)
3.多表的分页查询,子表条件(先根据子表条件查询子表,然后把主表id提取出来,拼接到wrapper中,这样就实现了子表条件动态拼接)
问题:第3种情况虽然可以实现子表条件的动态拼接,但是效率有点低,而且不适用复杂的情况
二、下面我们来看看进阶版的mybatisplus的联表分页查询
引入的依赖如下:
com.baomidoumybatis-plus-boot-starter3.5.2com.github.yulichangmybatis-plus-join1.3.11
4.我们先看一下数据库的数据,id为1,2,3的user数据分别对应car表中id为1,2,3,4,5,6,7的数据。下面的代码是mybatis-plus的联表查询和联表分页查询,一个user表对应多个car表
为什么要把两个查询都列出来,因为这里有个坑,我们先看一下联表查询的sql,以及最后返回的数据
这个是正常的联表查询,没有分页效果,因为是一对多的情况,所以sql查到的数据是16条,而不是user表中的12条,但是最后返回的list中是12条,而且一对多的关系也映射进去了,这个查询没有问题,但是联表分页查询时就有问题了
执行selectJoinPage方法后我们发现,查询到的总条数是12条,这个是单查主表数据用来拿到user的总条数,这个没什么问题,问题出现在后边的联表分页查询,经过之前的selectJoinList联表查询我们知道,正常的无条件一对多查询后的数据应该是16条,但是联表分页查询的时候,我们只拿到了10条数据,丢失了6条数据,这就是为什么最后查询到的数据只有6条的原因。
问题出现了我们要怎么解决呢?下面是我推荐的解决方案
注:只有联表分页查询的时候会出现这个情况,正常的单表分页查询是不会出现这个问题的;我们使用这个方法来判断是否出现一对多的情况
boolean resultMap = mpjLambdaWrapper.isResultMap();//是否存在一对多
然后我们执行一下看看效果
我们发现成功查询到了10条主表数据,总条数12条也没问题,关联关系也对上了,问题解决了
5.看到这我们已经基本的解决了联表分页查询主子表条件拼接的问题,以及返回值的主子表关联的问题。但是问题真的解决了么,细心的小伙伴会发现,我们的主题是动态的拼接sql,但是看代码会发现我们里面的实体对象UserExtend和UserExtend.class都是写死的,那么我们怎么实现动态的sql拼接呢?
这个是SFunctiionUtil工具类
package com.kuang.utils.SFunction的工具类;import cn.hutool.core.util.StrUtil;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.core.toolkit.support.SFunction;import java.lang.invoke.CallSite;import java.lang.invoke.LambdaMetafactory;import java.lang.invoke.MethodHandles;import java.lang.invoke.MethodType;import java.lang.reflect.Field;import java.util.*;import java.util.function.Function;import java.util.stream.Collectors;/**最后一次更新时间:2022-12-29 14:40 **/public class SFunctionUtil {/** 可序列化 */private static final int FLAG_SERIALIZABLE = 1;//mybatisplus的lambdaQueryWrapper在使用eq方法时,只能传入User::getId 不可以传入a->a.getId()//MyBatisPlus的条件构造器不会真的去调用SFunction这个函数式接口而是只解析实际的方法名.如果解析的是Lambbda表达式,//那么方法名跟数据库的列名匹配不上就会报错; 如果是方法引用那么方法名通过is/gey/set规则就能找到相应的字段名然后在根据规则转换成数据库表的列名./** * SFunctionUtil.getSFunctionByName(UserExtend.class,"getUserName",String.class)等同于UserExtend::getUserName * * @param entityClass 对象的classUserExtend.class * @param name想要转换的名称 UserExtend::getUserName中的 getUserName * @param typeClass userName 字段的类型 * @return */public static <T> SFunction<T,String> getSFunctionByName(Class<T> entityClass,String name,Class typeClass) {SFunction<T,String> func = null;final MethodHandles.Lookup lookup = MethodHandles.lookup();//po的返回Integer的一个方法MethodType methodType = MethodType.methodType(typeClass, entityClass);final CallSite site;try {//方法名叫做:getSecretLevel转换为 SFunction function interface对象site = LambdaMetafactory.altMetafactory(lookup,"invoke",MethodType.methodType(SFunction.class),methodType,lookup.findVirtual(entityClass, name, MethodType.methodType(typeClass)),methodType,FLAG_SERIALIZABLE);func = (SFunction) site.getTarget().invokeExact();} catch (Throwable e) {e.printStackTrace();}return func;}/** * 把对象中的每个字段的‘方法引用’->UserExtend::getUserName 返回的SFunction放到map中 * @param tClass 查询对象的class * @param * @return map.put("id",UserExtend::getId); * map.put("userName",UserExtend::getUserName); * @throws ClassNotFoundException */public static <T> Map<String,SFunction<T,String>> getSFunctionMapByClass(Class<T> tClass) {//1.通过plus中的wrapper获取到对象中的全部字段LambdaQueryWrapper<T> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.select(tClass,i->true);String sqlSelect = lambdaQueryWrapper.getSqlSelect();String[] underlineNameList = sqlSelect.split(",");//下滑线名字集合//2.组装一个map,map中存放方法名 和 对应字段的数据类型 map.put("userName",String.class);Map<String,Class> methodNameAndTypeClassMap = new HashMap<>();//获取所有的bean中所有的成员变量(包括父类)的名称以及类型List<Field> list = new ArrayList<>();Class classNew = null;try{classNew = Class.forName(tClass.getName());}catch (Exception e){e.printStackTrace();}while (classNew != null) {Field[] declaredFields = classNew.getDeclaredFields();for (Field field : declaredFields) {if(field.getName().equals("serialVersionUID")){continue;}if (!list.contains(field)) {list.add(field);}}classNew = (Class<T>) classNew.getSuperclass();}//根据字段名分组Map<String, Field> fieldMap = list.stream().collect(Collectors.toMap(Field::getName, Function.identity(), (a, b) -> a));for (String underlineName : underlineNameList) {String camelName = StrUtil.toCamelCase(underlineName);//下滑线转驼峰Field field = fieldMap.get(camelName);if(Objects.nonNull(field)){//获取到对应字段的数据类型class,放到methodNameAndTypeClassMap中String classTypeName = field.getGenericType().getTypeName();try{methodNameAndTypeClassMap.put(camelName,Class.forName(classTypeName));}catch (Exception e){e.printStackTrace();System.out.println(e);}}}//3.把每个字段都转成 SFunction函数Map<String,SFunction<T,String>> functionMap = new HashMap<>();for (Map.Entry<String, Class> entry : methodNameAndTypeClassMap.entrySet()) {String camelName = entry.getKey();String methodName = "get" + camelName.substring(0, 1).toUpperCase() + camelName.substring(1);//转成对应的get方法getUserName//getSFunctionByName(UserExtend.class,"getUserName",String.class);SFunction sFunction = getSFunctionByName(tClass, methodName, entry.getValue());functionMap.put(camelName,sFunction);}return functionMap;}}
这个是ClassUtilsNioa工具类
package com.kuang.utils.工具类;import cn.hutool.core.util.StrUtil;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.math.BigDecimal;import java.text.SimpleDateFormat;import java.util.*;import java.util.stream.Collectors;/** * 最后一次更新时间:2022-06-08 16:08:31 */public class ClassUtilsNioa {//获取到对象的第index个的字段名称 0为第一个字段public static <T> String getNameByIndex(T entity, int index) {return entity.getClass().getDeclaredFields()[index].getName();}//为对象中第index个字段赋值public static <T> void setVByIndex(T entity, int index, String setValue, String format) {String setName = getNameByIndex(entity, index);setV(entity, setName, setValue, format);}//1.批量对对象中的第index个字段赋值public static <T> void setVByIndexList(T entity, Map<Integer, String> map, String format) {for (Integer index : map.keySet()) {String setName = getNameByIndex(entity, index);setV(entity, setName, map.get(index), format);}}//2.批量对对象中的第index个字段赋值public static <T> void setVByIndexList(T entity, int[] indexList, String[] setValueList, String format) {if (indexList.length != setValueList.length) {return;}for (int i = 0; i < indexList.length; i++) {String setName = getNameByIndex(entity, indexList[i]);setV(entity, setName, setValueList[i], format);}}//为对象中的某个名称的字段赋值public static <T> void setV(T entity, String setName, String setValue) {setV(entity, setName, setValue, "");}//为对象中的某个名称的字段赋值public static <T> void setV(T entity, String setName, String setValue, String format) {Map<String, String> map = new HashMap<>();map.put(setName, setValue);setV(entity, map, format);}//批量为对象中的某个名称的字段赋值public static <T> void setV(T entity, Map<String, String> map) {setV(entity, map, "");}//批量为对象中的某个名称的字段赋值public static <T> void setV(T entity, Map<String, String> map, String format) {if (map == null || map.size() == 0) {return;}List<Field> list = getNameAll(entity);Map<String, List<Field>> fieldMap = list.stream().collect(Collectors.groupingBy(l -> l.getName()));Class clazz = entity.getClass();try {for (Map.Entry<String, String> entry : map.entrySet()) {String setName = entry.getKey();String setValue = entry.getValue();List<Field> fieldList = fieldMap.get(setName);if (fieldList != null && fieldList.size() > 0) {String type = fieldList.get(0).getGenericType().toString();setName = setName.substring(0, 1).toUpperCase() + setName.substring(1); // 将属性的首字符大写,方便构造get,set方法if (type.equals("class java.lang.String")) { // 如果type是类类型,则前面包含"class ",后面跟类名Method m = clazz.getMethod("set" + setName, String.class);m.invoke(entity, setValue);} else if (type.equals("class java.util.Date")) {Method m = clazz.getMethod("set" + setName, Date.class);Date date = new SimpleDateFormat((format == null || format.length() == 0) " />"yyyy-MM-dd" : format).parse(setValue);m.invoke(entity, date);} else if (type.equals("class java.lang.Integer")) {Method m = entity.getClass().getMethod("set" + setName, Integer.class);m.invoke(entity, Integer.valueOf(setValue));} else if (type.equals("class java.lang.Double")) {Method m = entity.getClass().getMethod("set" + setName, Double.class);m.invoke(entity, Double.valueOf(setValue));} else if (type.equals("class java.lang.Long")) {Method m = entity.getClass().getMethod("set" + setName, Long.class);m.invoke(entity, Long.valueOf(setValue));} else if (type.equals("class java.lang.Boolean")) {Method m = entity.getClass().getMethod("set" + setName, Boolean.class);m.invoke(entity, Boolean.valueOf(setValue));} else if (type.equals("class java.math.BigDecimal")) {Method m = entity.getClass().getMethod("set" + setName, BigDecimal.class);m.invoke(entity, new BigDecimal(setValue));} else if (type.equals("int")) {Method m = entity.getClass().getMethod("set" + setName, int.class);m.invoke(entity, Integer.valueOf(setValue));}}}} catch (Exception e) {e.printStackTrace();}}//获取到对象中第index个字段值public static <T> Object getVByIndex(T entity, int index) {String setName = getNameByIndex(entity, index);return getV(entity, setName);}//获取到对象中的某个名称的字段值public static <T> Object getV(T entity, String getName) {try {//获取到这个名字对应的FieldList<Field> nameAll = getNameAll(entity);Map<String, List<Field>> map = nameAll.stream().collect(Collectors.groupingBy(n -> n.getName()));List<Field> fieldList = map.get(getName);String type = fieldList.get(0).getGenericType().toString();// 将属性的首字符大写,方便构造get,set方法getName = getName.substring(0, 1).toUpperCase() + getName.substring(1);Class<?> clazz = entity.getClass();//不对基础类型进行get(因为一般的实体类没有int之类的类型,如果有父类中是有int等之类的,默认值是0,所以在get的时候不允许get出int之类的数据)if (type.equals("class java.lang.String") || type.equals("class java.util.Date") || type.equals("class java.lang.Integer") ||type.equals("class java.lang.Double") || type.equals("class java.lang.Long") || type.equals("class java.lang.Boolean") ||type.equals("class java.math.BigDecimal")) {getName = getName.substring(0, 1).toUpperCase() + getName.substring(1); // 将属性的首字符大写,方便构造get,set方法Method m = entity.getClass().getMethod("get" + getName);Object value = m.invoke(entity);return value;}else{return null;}} catch (Exception e) {throw new RuntimeException("没有get"+getName + "方法");}}//获取到某个对象的所有非空字段值集合public static <T> List<Object> getVList(T entity) {return getVList(entity, 0, entity.getClass().getDeclaredFields().length);}//获取到某个对象在某个区间的全部非空字段值集合public static <T> List<Object> getVList(T entity, int begin, int end) {List<Object> objList = new ArrayList<>();Field[] fieldList = entity.getClass().getDeclaredFields();if (end > fieldList.length) {return null;}for (int i = begin; i < end; i++) {String name = fieldList[i].getName();Object v = getV(entity, name);if (!Objects.isNull(v)) {objList.add(v);}}return objList;}//获取到某个对象的全部非空字段值集合(包括父类)public static <T> List<Object> getVListAndSuper(T entity) throws Exception {List<Field> list = getNameAll(entity);//全部的字段名称集合List<Object> objList = new ArrayList<>();//非空字段集合for (Field field : list) {String name = field.getName();name = name.substring(0, 1).toUpperCase() + name.substring(1); // 将属性的首字符大写,方便构造get,set方法Method m = entity.getClass().getMethod("get" + name);Object value = m.invoke(entity); // 调用getter方法获取属性值if (!Objects.isNull(value)) {objList.add(value);}}return objList;}//获取到某个对象的全部字段集合public static <T> List<Field> getName(T entity) {Class clazz = entity.getClass();//获取所有的bean中所有的成员变量(包括父类)的名称以及类型List<Field> list = new ArrayList<>();Field[] declaredFields = clazz.getDeclaredFields();for (Field field : declaredFields) {if(field.getName().equals("serialVersionUID")){continue;}if (!list.contains(field)) {list.add(field);}}return list;}//获取到某个对象的全部字段集合(包括父类)public static <T> List<Field> getNameAll(T entity) {Class clazz = entity.getClass();//获取所有的bean中所有的成员变量(包括父类)的名称以及类型List<Field> list = new ArrayList<>();while (clazz != null) {Field[] declaredFields = clazz.getDeclaredFields();for (Field field : declaredFields) {if(field.getName().equals("serialVersionUID")){continue;}if (!list.contains(field)) {list.add(field);}}clazz = clazz.getSuperclass();}return list;}//获取到某个对象的所有非空字段值集合 返回Mappublic static <T> Map<String, Object> getVMapList(T entity) {return getVMapList(entity, 0, entity.getClass().getDeclaredFields().length, true);}//获取到某个对象的所有非空字段值集合 返回Mappublic static <T> Map<String, Object> getVMapList(T entity, boolean flag) {return getVMapList(entity, 0, entity.getClass().getDeclaredFields().length, flag);}//获取到某个对象在某个区间的全部非空字段值集合返回Mapflag是判断是否转为下划线 true->转化为下划线 false->保持原来的值默认为truepublic static <T> Map<String, Object> getVMapList(T entity, int begin, int end, boolean flag) {Map<String, Object> map = new HashMap<>();Field[] fieldList = entity.getClass().getDeclaredFields();if (end > fieldList.length) {return null;}for (int i = begin; i < end; i++) {String name = fieldList[i].getName();if ("serialVersionUID".equals(name)) {continue;}Object v = getV(entity, name);if (!Objects.isNull(v)) {if (flag) {String simpleName = StrUtil.toUnderlineCase(name);//驼峰转下划线map.put(simpleName, v);} else {map.put(name, v);}}}return map;}//获取到某个对象的所有非空字段值集合 返回Map(包括父类)flag:为空是否put默认否public static <T> Map<String, Object> getVMapListAndSuper(T entity){return getVMapListAndSuper(entity, false);}//获取到某个对象的所有非空字段值集合 返回Map(包括父类)flag:为空是否puttrue:是false:否public static <T> Map<String, Object> getVMapListAndSuper(T entity, boolean flag){List<Field> list = getNameAll(entity);//全部的字段Field集合Map<String, Object> map = new HashMap<>();//for (Field field : list) {String name = field.getName();if ("serialVersionUID".equals(name)) {continue;}Object value = getV(entity, name);if (Objects.isNull(value)) {//如果为空判断flag如果是true则put一个空字符串如果是false则不进行操作if (flag) {value = "";map.put(name, value);}} else {//如果不为空,正常putmap.put(name, value);}}return map;}}
下面是完整的代码
//1.首先前台传给我们的json字符串,我们先转换成map形式//多表分页查询的条件 {"userName":"nioa1","car.name":"黑色"}Map<String, Object> conditionMap = new HashMap<>();conditionMap.put("userName","nioa1");conditionMap.put("car.colour","黑色");//2.我们在controller层先写好一对多关系的wrapper,这个位置我们是知道具体的class的Class primaryTableClass = UserExtend.class;//主表的classMPJLambdaWrapper<UserExtend> mpjLambdaWrapper = new MPJLambdaWrapper<UserExtend>().selectAll(UserExtend.class).selectCollection(CarExtend.class,UserExtend::getCarExtendList).leftJoin(CarExtend.class,CarExtend::getPkUser,UserExtend::getId);//3.接下来我们需要做的就是动态的拼接sql了,在拼接前我们需要做一下准备操作,把主子表的字段对应的方法引用都提取出来//也就是说我们要把UserExtend和CarExtend中的字段的方法引用都拿出来,但是问题来了我们怎么知道他的主子表都有谁呢?//原来mybatis-plus在构建的时候就可以拿到所有的表信息(详细的过程可以去看一下官方的源码,我们这里只需要了解到这个方法可以获取到所有用了plus的实体即可)//包括但不限于UserExtend.class和CarExtend.class这两个反射类,只要是实现了plus的接口的实体都会在这里面 TableInfoHelper.getTableInfos()//4.我们先定义一个map缓存,可以在实例化的时候默认执行这个,可以不用每次都手动去转换了,使用的时候只需要用map中的数据就行/** *用来储存每个表中的方法引用 *key: mybatis-plus缓存中的所有表名 *value: 每个表对应实体中的‘方法引用’ UserExtend::getIdUserExtend::getUserName *Map<String, SFunction --> map.put("id",UserExtend::getId); */Map<String,Map<String, SFunction<?, String>>> tableSFunctionCache = new ConcurrentHashMap<>();/** *用来储存每个表中的方法引用 *key: mybatis-plus缓存中的所有表class *value: 每个表对应实体中的‘方法引用’ UserExtend::getIdUserExtend::getUserName *Map<String, SFunction --> map.put("id",UserExtend::getId); */Map<Class,Map<String, SFunction<?, String>>> tableClassSFunctionCache = new ConcurrentHashMap<>();//获取所有实体映射表信息List<TableInfo> tableInfoList = TableInfoHelper.getTableInfos();if(CollectionUtil.isEmpty(tableInfoList)){return;}for (TableInfo tableInfo : tableInfoList) {String tableName = tableInfo.getTableName();//表名称Class entityType = tableInfo.getEntityType();//实体类型Map<String, SFunction<?, String>> functionMap = SFunctionUtil.getSFunctionMapByClass(entityType);//tableSFunctionCache.put(tableName,functionMap);tableClassSFunctionCache.put(entityType,functionMap);}//注:上边的操纵可以理解为 两个map一个是用来存名字的,一个是用来存class的//Map<String,SFunction> userFunctionMap = new HashMap();//userFunctionMap.put("id",UserExtend::getId);//userFunctionMap.put("userCode",UserExtend::getUserCode);//userFunctionMap.put("userName",UserExtend::getUserName);//Map<String,SFunction> carFunctionMap = new HashMap();//carFunctionMap.put("id",CarExtend::getId);//carFunctionMap.put("pkUser",CarExtend::getPkUser);//carFunctionMap.put("name",CarExtend::getName);////Map<String,Map<String, SFunction>> classFunctionMap = new HashMap();//classFunctionMap.put("user",userFunctionMap);//classFunctionMap.put("car",carFunctionMap);//5.当我们拿到所有表的每个方法引用后,我们就可以去拼接sql了for (Map.Entry<String, Object> entry : conditionMap.entrySet()) {String tableName = entry.getKey();//表名Object value = entry.getValue();//条件String[] split = tableName.split("\\.");if(split.length == 1){//如果长度是1,那么这个字段是主表的String primaryFieldName =split[0];//主表的字段名Map<String, SFunction<?, String>> primaryFunctionMap = tableClassSFunctionCache.get(primaryTableClass);SFunction<?, String> fileSFunction = primaryFunctionMap.get(primaryFieldName);mpjLambdaWrapper.eq(fileSFunction,value);}else{//如果长度是2,那么这个字段是子表的String sublistTableName =split[0];//子表的表名String sublistFieldName =split[1];//子表的字段名Map<String, SFunction<?, String>> tableFunctionMap = tableSFunctionCache.get(sublistTableName);//子表的方法引用SFunction<?, String> fileSFunction = tableFunctionMap.get(sublistFieldName);mpjLambdaWrapper.eq(fileSFunction,value);}}//6.好了我们的主子表条件已经动态拼接完成了,接下来就是分页查询了,我们把之前的代码复制过来就行了//如果出现了一对多的情况PageImpl page;if(mpjLambdaWrapper.isResultMap()){SFunction<?, String> idFunction = tableClassSFunctionCache.get(primaryTableClass).get("id");//根据主表的查询条件和分页的值,获取到对应主表的id集合MPJLambdaWrapper wrapper = new MPJLambdaWrapper();wrapper.select(tableClassSFunctionCache.get(primaryTableClass).get("id"));//这个代码与上边的区别是没有子表条件了,因为要查询总数是查询的主表数据,没有子表的事,传入条件也只是主表的条件for (Map.Entry<String, Object> entry : conditionMap.entrySet()) {String tableName = entry.getKey();//表名Object value = entry.getValue();//条件String[] split = tableName.split("\\.");if(split.length == 1){//如果长度是1,那么这个字段是主表的String primaryFieldName =split[0];//主表的字段名Map<String, SFunction<?, String>> primaryFunctionMap = tableClassSFunctionCache.get(primaryTableClass);SFunction<?, String> fileSFunction = primaryFunctionMap.get(primaryFieldName);wrapper.eq(fileSFunction,value);}}IPage ipage = userMapper.selectJoinPage(new Page<>(0, 10),primaryTableClass,wrapper);long total = ipage.getTotal();//总条数List records = ipage.getRecords();List<String> idList = (List) records.stream().map(a -> ClassUtilsNioa.getV(a,"id")).collect(Collectors.toList());//把id集合拼接到查询条件中,然后不走联表分页查询,只用普通的分页查询即可mpjLambdaWrapper.in(idFunction,idList);List userExtendList = userMapper.selectJoinList(primaryTableClass, mpjLambdaWrapper);page = new PageImpl(userExtendList, PageRequest.of(0, 10), total);}else{IPage userExtendIPage = userMapper.selectJoinPage(new Page<>(0, 10), UserExtend.class, mpjLambdaWrapper);page = new PageImpl(userExtendIPage.getRecords(), PageRequest.of(0, 10), userExtendIPage.getTotal());}//这样我们成功的完成了联表分页查询+动态拼接主子表条件啦System.out.println(JSON.toJSONString(page));
以上就是我们全部的内容了,本文只是提供一种思路,小伙伴们可以根据代码中的工具类去定制自己的联表分页查询啦