项目实战六-考试模块-科2科3

本节主要讲多表查询

项目初始化

  1. 项目分析
    1. 页面如下 图1
    2. 根据exam_place_course表,通过EasyCode-MybatisCodeHepler生成对应层的类

       id create_time  name  price  intro  video  cover place_id
      
      1. place_id是关联exam_palce这张表的id的,因此这张表里面没有省份id、城市id
      2. 需要通过place_id找到exam_palce对应的id,然后找到plage_region对应的省份、城市id
  2. 各个层代码如下
    1. po

       @Data
       public class ExamPlaceCourse {
           /**
           * 主键
           */
           private Integer id;
                  
           private Date createTime;
           /**
           * 名称
           */
           private String name;
           /**
           * 价格
           */
           private Double price;
           /**
           * 课程类型:0是课程合集,2是科目2,3是科目3
           */
           private Short type;
                  
           private String intro;
           /**
           * 视频
           */
           private String video;
           /**
           * 封面
           */
           private String cover;
           /**
           * 考场
           */
           @ForeignField(ExamPlace.class)
           private Integer placeId;
       }
      
    2. query

       @EqualsAndHashCode(callSuper = true)
       @Data
       public class ExamPlaceCourseQuery extends KeywordQuery {
           private Short type;
           private Integer provinceId;
           private Integer cityId;
           private Integer placeId;
       }
      
    3. mapper

       public interface ExamPlaceCourseMapper extends BaseMapper<ExamPlaceCourse> {}
      
    4. service

       //ExamPlaceCourseService
       public interface ExamPlaceCourseService extends IService<ExamPlaceCourse>{
           void list(ExamPlaceCourseQuery query);
       }
       //ExamPlaceCourseServiceImpl
       @Service
       @Transactional
       public class ExamPlaceCourseServiceImpl extends ServiceImpl<ExamPlaceCourseMapper, ExamPlaceCourse> implements ExamPlaceCourseService {
              
           @Override
           public void list(ExamPlaceCourseQuery query) {
               MpQueryWrapper<ExamPlaceCourse> wrapper = new MpQueryWrapper<>();
               //先将数据列表展示,查询代码空着
               baseMapper.selectPage(new MpPage<>(query),wrapper).updateQuery();
           }
       }
      
    5. controller

       @RestController
       @RequestMapping("/examPlaceCourses")
       public class ExamPlaceCourseController extends BaseController<ExamPlaceCourse> {
           @Autowired
           private ExamPlaceCourseService service;
              
           @Override
           protected IService<ExamPlaceCourse> getService() {
               return service;
           }
           @GetMapping
           public R list(ExamPlaceCourseQuery query) {
               service.list(query);
               return Rs.ok(query);
           }
       }
      
    6. 此时页面能正常展示列表

实现三级联动

  1. 搜索条件可以选择省份、城市、考场,属于三级联动,因此需要给客户端提供三级联动的数据,三级联动的后台数据如下

     {
         "code": 0,
         "data": [
             {
                 "id": 3,
                 "name": "广东",
                 "plate": "粤",
                 "children": [
                     {
                         "id": 5,
                         "name": "广州",
                         "plate": "A",
                         "children": [
                             {
                                 "id": 1,
                                 "name": "白云考场"
                             },
                             {
                                 "id": 5,
                                 "name": "天河考场"
                             }
                         ]
                     },
                     {
                         "id": 12,
                         "name": "深圳",
                         "plate": "B",
                         "children": [
                             {
                                 "id": 4,
                                 "name": "保安考场"
                             }
                         ]
                     }
                 ]
             },
             {
                 "id": 10,
                 "name": "广西",
                 "plate": "桂",
                 "children": [
                     {
                         "id": 11,
                         "name": "南宁",
                         "plate": "A",
                         "children": [
                             {
                                 "id": 3,
                                 "name": "电瓶车考场"
                             }
                         ]
                     }
                 ]
             }
         ]
     }
    
  2. 从上面数据分析可以得知,需要在CityDto下新增children字段

     @Data
     public class CityDto {
         private Integer id;
         private String name;
         private String plate;
         private List<?> children;
     }
    
  3. 于是考场信息ExamPlaceController下新增

     @GetMapping("/regionExamPlaces")
     public R listRegionExamPlaces() {
         return Rs.ok(service.listRegionExamPlaces());
     }
    
  4. ExamPlaceService

     //ExamPlaceService新增
     List<ProvinceDto> listRegionExamPlaces();
     //ExamPlaceServiceImpl新增
      @Override
     public List<ProvinceDto> listRegionExamPlaces() {
         return baseMapper.selectRegionExamPlaces();
     }
    
  5. resources.com.zh.jk.mapper下新增ExamPlaceMapper.xml

     <?xml version="1.0" encoding="UTF-8" ?>
     <!DOCTYPE mapper
             PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
     <mapper namespace="com.zh.jk.mapper.ExamPlaceMapper">
         <resultMap id="rmSelectRegionExamPlaces"
                    type="ProvinceDto">
             <id column="id" property="id"/>
             <result column="name" property="name"/>
             <result column="plate" property="plate"/>
             <collection property="children" ofType="CityDto">
                 <id column="city_id" property="id"/>
                 <result column="city_name" property="name"/>
                 <result column="city_plate" property="plate"/>
                 <collection property="children" ofType="ExamPlace">
                     <id column="exam_place_id" property="id"/>
                     <result column="exam_place_name" property="name"/>
                 </collection>
             </collection>
         </resultMap>
        
         <select id="selectRegionExamPlaces"
                 resultMap="rmSelectRegionExamPlaces">
             SELECT
                 p.id,
                 p.name,
                 p.plate,
                 c.id city_id,
                 c.name city_name,
                 c.plate city_plate,
                 e.id exam_place_id,
                 e.name exam_place_name
             FROM plate_region p
                  JOIN plate_region c ON c.parent_id = p.id
                  JOIN exam_place e ON e.city_id = c.id AND e.province_id = p.id
             WHERE p.parent_id = 0
             ORDER BY p.pinyin, c.plate
         </select>
     </mapper>
    
  6. 则可以通过http://localhost:8888/examPlaces/regionExamPlaces接口获取三级联动数据

有条件多表查询

注意,本节需要先看下一节的项目重构,然后再继续下面

  1. 当前列表页面的数据显示的是直接查询,不分页查询的结果,而且是没有条件查询
  2. 点击编辑,页面如下 图1
  3. 省份为何为空呢?从数据库表exam_place_course可以知道,该表是没有省份、城市字段的,因此也无法做简单的条件查询,因此当点击列表中的某条数据,是拿不到省份、城市信息的,编辑页面也不能携带到省份、城市信息
  4. 解决办法
    1. 改造响应给客户端的数据,通过多表查询,将数据返回全面
    2. 每条数据除了返回最基本的展示数据外,还要将该考试项目对应的省份、城市、场地,都查询出来,然后返回给客户端,然后点击编辑的时候才能正常展示省份、城市、考场
    3. 总之就是将多张表的数据查询出来,封装成一条完整的数据
  5. 改造步骤如下
    1. 返回数据ExamPlaceCourseVo

       //新增省份、城市id
       @Data
       public class ExamPlaceCourseVo {
           /**
           * 主键
           */
           private Integer id;
              
           /**
           * 名称
           */
           private String name;
           /**
           * 价格
           */
           private Double price;
           /**
           * 课程类型:0是课程合集,2是科目2,3是科目3
           */
           private Short type;
                  
           private String intro;
              
           /**
           * 考场id
           */
           private Integer placeId;
              
           private Integer provinceId;
              
           private Integer cityId;
       }
      
    2. 改造dao层
      1. 由于Mybatis-plus只有默认的单表查询方法,因此需要自定义方法与实现
      2. ExamPlaceCourseMapper新增自定义方法

         public interface ExamPlaceCourseMapper extends BaseMapper<ExamPlaceCourse> {
             /*
             第一个参数page:
             会自动在xml的sql语句最后面自动添加LIMIT分页查询条件
             第二个参数wapper:
             带@Param取别名修饰的参数,是可以传递到xml中去的,然后使用,之前讲过!!!
             Constants.WRAPPER为ew,即给wrapper参数取的别名为ew
             Wrapper的类型为Mybatis-plus内部封装的wrapper的最基层类,是LambdaQueryWrapper、QueryWrapper的父类
             在xml中可以使用该类的customSqlSegment方法,获取查询条件
             ${ew.customSqlSegment} 等价于 WHERE XXXX AND XXX ...
             * */
             MpPage<ExamPlaceCourseVo> selectPageVos(MpPage<ExamPlaceCourseVo> page,
                                                     @Param(Constants.WRAPPER) Wrapper<ExamPlaceCourseVo> wrapper);
         }
        
      3. 在resources.com.zh.jk.mapper下新建ExamPlaceCourseMapper.xml

         <?xml version="1.0" encoding="UTF-8" ?>
         <!DOCTYPE mapper
                 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
         <mapper namespace="com.zh.jk.mapper.ExamPlaceCourseMapper">
             <resultMap id="rmSelectPageVos"
                        type="ExamPlaceCourseVo">
                 <id property="id" column="id"/>
                 <result property="name" column="name"/>
                 <result property="price" column="price"/>
                 <result property="type" column="type"/>
                 <result property="placeId" column="place_id"/>
                 <result property="provinceId" column="province_id"/>
                 <result property="cityId" column="city_id"/>
             </resultMap>
                    
             <select id="selectPageVos"
                     resultMap="rmSelectPageVos">
                 SELECT
                     c.id,
                     c.name,
                     c.price,
                     c.type,
                     c.intro,
                     c.place_id,
                     p.province_id,
                     p.city_id
                 FROM exam_place_course c
                     JOIN exam_place p ON p.id = c.place_id
                     ${ew.customSqlSegment}
             </select>
         </mapper>
        
    3. 改造service的impl层

       @Service
       @Transactional
       public class ExamPlaceCourseServiceImpl extends ServiceImpl<ExamPlaceCourseMapper, ExamPlaceCourse> implements ExamPlaceCourseService {
              
           @Override
           public PageVo<ExamPlaceCourseVo> list(ExamPlaceCoursePageReqVo query) {
               MpQueryWrapper<ExamPlaceCourseVo> wrapper = new MpQueryWrapper<>();
               Integer placeId = query.getPlaceId();
               Integer provinceId = query.getProvinceId();
               Integer cityId = query.getCityId();
               Short type = query.getType();
               // 类型
               if (type != null && type >= 0) {
                   wrapper.eq(ExamPlaceCourseVo::getType, type);
               }
               // 考场 -> 城市 -> 省份
               if (placeId != null && placeId > 0) {
                   wrapper.eq(ExamPlaceCourseVo::getPlaceId, placeId);
               } else if (cityId != null && cityId > 0) {
                   wrapper.eq(ExamPlaceCourseVo::getCityId, cityId);
               } else if (provinceId != null && provinceId > 0) {
                   wrapper.eq(ExamPlaceCourseVo::getProvinceId, provinceId);
               }
               //关键词搜索
               wrapper.like(query.getKeyword(),ExamPlaceCourseVo::getName,ExamPlaceCourseVo:: getIntro);
               return baseMapper.selectPageVos(new MpPage<>(query),wrapper)
                      .buildVo();
           }
       }
      
    4. 运行项目,然后在科2科3模块列表页面点击科目(type)搜索,会崩溃

       //引起原因是ew.customSqlSegment方法
       Caused by: org.apache.ibatis.builder.BuilderException: Error evaluating expression 'ew.customSqlSegment'. Cause: org.apache.ibatis.ognl.OgnlException: customSqlSegment [com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: can not find lambda cache for this entity [com.zh.jk.pojo.vo.list.ExamPlaceCourseVo]]
              
       //引起原因是customSqlSegment方法
       Caused by: org.apache.ibatis.ognl.OgnlException: customSqlSegment
              
       //引起原因是com.zh.jk.pojo.vo.list.ExamPlaceCourseVo没有lambda cache
       Caused by: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: can not find lambda cache for this entity [com.zh.jk.pojo.vo.list.ExamPlaceCourseVo]
      
      1. 本质就是说xml里面的${ew.customSqlSegment} 方法错误
      2. 本质原因是ExamPlaceCourseVo类can not find lambda cache

报错原因分析

  1. ExamPlaceCourseMapper的类继承的BaseMapper,里面传递的泛型是ExamPlaceCourse类型
  2. 通过ExamPlaceCourseServiceImpl类中调用的方法是

     //本质是调用ExamPlaceCourseMapper的selectPageVos方法
     baseMapper.selectPageVos(new MpPage<>(query),wrapper)
                        .buildVo();
    
  3. wrapper参数对应的就是ExamPlaceCourseMapper的wrapper,对应xml里面的就是ew这个对象
  4. ew.customSqlSegment取值本质是通过BaseMapper<ExamPlaceCourse>里面的泛型类(ExamPlaceCourse)来取值,而不是通过ExamPlaceCourseVo类来取值,即只有BaseMapper<ExamPlaceCourse>里面的泛型类才有lambda cache
  5. 那么我们如何才能给ExamPlaceCourseVo也添加lambda cache呢?
  6. 在MyBatisPlusCfg下改造
    1. 实现InitializingBean接口
    2. 实现方法

       //只添加一次
       @Override
       public void afterPropertiesSet() throws Exception {
           /*
           //解决:can not find lambda cache for this entity报错异常
           首先,拥有lambda cache的实体类Entity,才能使用LambdaQueryWrapper<Entity>,
           默认情况下,只有BaseMapper<Entity>中的Entity类,才拥有lambda cache,
           比如:ExamPlaceCourseMapper继承BaseMapper<ExamPlaceCourse>,ExamPlaceCourse这个类才拥有lambda cache
           其他类需要通过TableInfoHelper手动添加lambda cache
           由于ExamPlaceCourseVo没有lambda cache,因此需要手动添加
           */
           MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
           TableInfoHelper.initTableInfo(assistant, ExamPlaceCourseVo.class);
       }
      
  7. 重新运行项目,仍然报错

     // 通过province_id查询时:Unknown column 'province_id' in 'where clause'
     // 通过province_id查询时:Unknown column 'city_id' in 'where clause'
     // 通过name查询时:Column 'name' in where clause is ambiguous
    

报错原因分析2

  1. impl中使用的是MpQueryWrapper来查询的,MpQueryWrapper继承自LambdaQueryWrapper
  2. LambdaQueryWrapper的查询方式是

     wrapper.eq(ExamPlaceCourseVo::getPlaceId, placeId);
     wrapper.eq(ExamPlaceCourseVo::getCityId, cityId);
     wrapper.eq(ExamPlaceCourseVo::getProvinceId, provinceId);
     wrapper.like(query.getKeyword(),ExamPlaceCourseVo::getName,ExamPlaceCourseVo:: getIntro);
    
  3. ExamPlaceCourseVo::getCityId的本质就是“city_id”等价于如下

     wrapper.eq("place_id", placeId);
     wrapper.eq("city_id", cityId);
     wrapper.eq("province_id", provinceId);
     wrapper.like(query.getKeyword(),"name",ExamPlaceCourseVo:: getIntro);
    
  4. 这些默认的都是在exam_place_course这一张表中去查找,不会到exam_place表中去查找,因此会出现

     Unknown column 'province_id' in 'where clause'
     Unknown column 'city_id' in 'where clause'
    
  5. 由于exam_place_course与exam_place都有name字段,不知道具体查询的是哪张表的name因此会出现

     Column 'name' in where clause is ambiguous(模棱两可歧义)
    
  6. 由于LambdaQueryWrapper只能传参ExamPlaceCourseVo::getCityId格式(city_id),因此不能使用LambdaQueryWrapper的体系类来查询
    1. 那么能不能传参(”p.city_id”)指定到哪张表中去查找呢? —- QueryWrapper
  7. 可以使用QueryWrapper,impl的list方法改造如下

     QueryWrapper<ExamPlaceCourseVo> wrapper = new QueryWrapper<>();
     Integer placeId = query.getPlaceId();
     Integer provinceId = query.getProvinceId();
     Integer cityId = query.getCityId();
     Short type = query.getType();
     // 类型
     if (type != null && type >= 0) {
         wrapper.eq("c.type", type);
     }
    
     // 考场 -> 城市 -> 省份
     if (placeId != null && placeId > 0) {
         wrapper.eq("c.place_id", placeId);
     } else  if (cityId != null && cityId > 0) {
         wrapper.eq("p.city_id", cityId);
     } else if (provinceId != null && provinceId > 0) {
         wrapper.eq("p.province_id", provinceId);
     }
    
     // 关键词
     String keyword = query.getKeyword();
     if (!Strings.isEmpty(keyword)){
        wrapper.nested((w)->{
            //这里是查表c的name,至于c哪来的,对应xml中的sql语句中
            w.like("c.name",keyword).or().like("c.intro",keyword);
        });
     }
     return baseMapper
             .selectPageVos(new MpPage<>(query), wrapper)
             .buildVo();
    

最终改造

  1. 由于直接使用QueryWrapper调用方法比较复杂,则可以封装一个子类
  2. 之前MpQueryWrapper类名修改为MpLambdaQueryWrapper,因为是继承LambdaQueryWrapper
    1. 同时修改掉所有Impl里面用MpQueryWrapper的修改为MpLambdaQueryWrapper
  3. 重新建立一个MpQueryWrapper继承自QueryWrapper,并分装好调用方法

     //com.zh.jk.common.enhance
     public class MpQueryWrapper<T> extends QueryWrapper<T> {
         public final MpQueryWrapper<T> like(Object val, String... columns) {
             if (val == null) return this;
             String str = val.toString();
             if (str.length() == 0) return this;
             return (MpQueryWrapper<T>) nested((w) -> {
                 for (String column : columns) {
                     w.like(column, str).or();
                 }
             });
         }
     }
    
  4. impl的改造如下

     MpQueryWrapper<ExamPlaceCourseVo> wrapper = new MpQueryWrapper<>();
     Integer placeId = query.getPlaceId();
     Integer provinceId = query.getProvinceId();
     Integer cityId = query.getCityId();
     Short type = query.getType();
     // 类型
     if (type != null && type >= 0) {
         wrapper.eq("c.type", type);
     }
    
     // 考场 -> 城市 -> 省份
     if (placeId != null && placeId > 0) {
         wrapper.eq("c.place_id", placeId);
     } else  if (cityId != null && cityId > 0) {
         wrapper.eq("p.city_id", cityId);
     } else if (provinceId != null && provinceId > 0) {
         wrapper.eq("p.province_id", provinceId);
     }
    
     // 关键词
     //这里是查表c的name,至于c哪来的,对应xml中的sql语句中
     wrapper.like(query.getKeyword(),"c.name","c.intro");
    
     return baseMapper
             .selectPageVos(new MpPage<>(query), wrapper)
             .buildVo();
    

总结

  1. MpLambdaQueryWrapper(LambdaQueryWrapper)用来做单表查询条件工具类,只能在当前表查询
  2. MpQueryWrapper(QueryWrapper)用来做多表查询的条件工具类,可以自定到其他表查询
  3. 只有条件查询才会用到wrapper,因此ProvinceVo不需要wapper
  4. 多表查询不能使用Mybatis-plus自己封装的方法,只能自定义方法
    1. mapper自定义方法声明
    2. xml中方法实现
Table of Contents