赞
踩
说明:
(1)本篇博客比较重要的点:
●【参数没有放在body中,而是放在了url中】+【后端使用实体类去接收参数】:对于这种情况,不能使用@RequestBody注解,自然也不能使用@RequestParam注解;(只是,我们在写实体类的时候,实体类的属性要比照着接口参数写,不能写错)
● 对于那些条件比较多的接口,专门创建一个Query对象,来组织条件;是一个不错的开发技巧;
● 对于排序方式这种查询条件,我们最好根据接口要求,在程序中规定好【究竟有哪些排序条件】;这能很好的防止接口乱调用,程序也更加安全;
● 本篇博客也涉及到了【mybatis动态SQL,<if></if>】、【MySQL的模糊查询,like】、【MySQL的in子句】、【mybatis的批处理中的<foreack>子标签】等;
(2)本篇博客一个待进一步研究的地方:这人的查询,我们使用的是MySQL的原生查询方式;但是,如果以后我们的项目和大数据结合的时候,似乎可以了解一下【ElasticSearch】这些技术,来更好的实现数据的检索和查询;
目录
1.在ProductController类中,创建查询前台的商品列表的方法:list()方法;
(1)因为前台的【商品列表】接口的参数很多,所以我们创建了一个实体类ProductListReq类,用这个实体类来接收参数;
(3)因为根据接口文档中对接口返回数据格式的要求,我们需要使用PageHelper的PageInfo来包装分页数据,所以我们在开发Service层的方法时,其返回类型就是PageInfo格式的;
2.1.在ProductServiceImpl类中,创建以分页的方式查询符合条件的商品的方法:list()方法;(重点!!!)
(1.2)【categoryId,keyword】这两个查询条件处理:构建Query对象之:判断是否有【搜索关键词】这个条件:如果有,就把这个条件构建到Query对象上去;
(1.3)【categoryId,keyword】这两个查询条件处理:构建Query对象之:判断是否有【商品分类Id】这个条件:如果有,就把这个条件处理一下,然后构建到Query对象上去;
(2)【orderBy】这一个条件处理:在构建PageHelper的时候,应用这个排序条件;需要注意:这儿我们需要【在程序中,规定好有哪些字段可以排序】,这非常重要,有关程序的安全;
(3)以前面构建的Query对象作为条件,去调用我们在Dao层编写查询方法:selectList()方法;;这个方法,在下一部分介绍;
(4)把查询结果,包装成PageInfo对象,返回给调用方;
2.2.在ProductService接口中,反向生成方法的声明;
3.1在ProductMapper中,声明根据条件,查询商品的方法:selectList()方法;
3.2在ProductMapper.xml中,编写对应的实现SQL;
1.前台的【商品列表】接口,在界面上的表现;
(1)我们可以不使用任何条件,来分页显示商品数据;
……………………………………………………
(2)我们可以使用【按价格从低到高或从高到低排序】这个条件,来分页显示商品数据;
……………………………………………………
(3)我们可以使用【商品名需要包含某个关键词】这个条件,来分页显示商品数据;与此同时,也可以加入【按价格从低到高或从高到低排序】这个条件;
……………………………………………………
(4)我们可以使用【商品需要隶属于“某商品分类及其子分类”】这个条件,来分页显示商品数据;与此同时,也可以加入【按价格从低到高或从高到低排序】这个条件;
PS:上图少标注了一个,id=19的果冻橙这个分类,是id=4的橘子橙子这个分类的子目录;;;所以,id=36的四川果冻橙要需要查询出来;
可以看到,前台的【商品列表】接口,这一个接口需要承担的任务还是挺多的;这就需要我们在开发这个接口时,做好条件判断和处理;
2.前台的【商品列表】接口文档;
……………………………………………………
以这个请求实例为例,来仔细说下前台的【商品列表】接口文档;
接口返回内容:
{ "status": 10000, "msg": "SUCCESS", "data": { "total": 3, "list": [ { "id": 24, "name": "智利帝王蟹礼盒装4.4-4.0斤/只 生鲜活鲜熟冻大螃蟹", "image": "http://111.231.103.117:8081/images/diwangxie.jpg", "detail": "商品毛重:3.0kg商品产地:智利大闸蟹售卖方式:公蟹重量:2000-4999g套餐份量:5人份以上国产/进口:进口海水/淡水:海水烹饪建议:火锅,炒菜,烧烤,刺身,加热即食包装:简装原产地:智利保存状态:冷冻公单蟹重:5.5两及以上分类:帝王蟹特产品类:其它售卖方式:单品", "categoryId": 7, "price": 222, "stock": 222, "status": 1, "createTime": "2019-12-28T08:06:34.000+0000", "updateTime": "2020-02-10T16:05:05.000+0000" }, { "id": 21, "name": "智利原味三文鱼排(大西洋鲑)240g/袋 4片装", "image": "http://111.231.103.117:8081/images/sanwenyu2.jpg", "detail": "商品毛重:260.00g商品产地:中国大陆保存状态:冷冻国产/进口:进口包装:简装类别:三文鱼海水/淡水:海水烹饪建议:煎炸,蒸菜,烧烤原产地:智利", "categoryId": 8, "price": 499, "stock": 1, "status": 1, "createTime": "2019-12-28T07:13:07.000+0000", "updateTime": "2020-02-10T15:38:46.000+0000" }, { "id": 22, "name": "即食海参大连野生辽刺参 新鲜速食 特级生鲜海产 60~80G", "image": "http://111.231.103.117:8081/images/haishen.jpg", "detail": "商品毛重:1.5kg商品产地:中国大陆贮存条件:冷冻重量:50-99g国产/进口:国产适用场景:养生滋补包装:袋装原产地:辽宁年限:9年以上等级:特级食品工艺:冷冻水产热卖时间:9月类别:即食海参固形物含量:70%-90%特产品类:大连海参售卖方式:单品", "categoryId": 13, "price": 699, "stock": 3, "status": 1, "createTime": "2019-12-28T07:16:29.000+0000", "updateTime": "2020-02-10T16:04:29.000+0000" } ], "pageNum": 1, "pageSize": 10, "size": 3, "startRow": 1, "endRow": 3, "pages": 1, "prePage": 0, "nextPage": 0, "isFirstPage": true, "isLastPage": true, "hasPreviousPage": false, "hasNextPage": false, "navigatePages": 8, "navigatepageNums": [ 1 ], "navigateFirstPage": 1, "navigateLastPage": 1 } }这儿主要说明两点:
● 这儿显示的商品都是扁平的,没有嵌套;
● 根据接口文档对返回数据的格式要求,我们可以发现,需要使用PageHelper分页组件的PageInfo对象来组织返回的分页数据;
3. 前台的【商品列表】接口,开发分析;
说明:
(1)前台的【商品列表】接口,这儿主要的地方就是根据不同的条件,去搜索的功能;
(2)入参判空;判断参数是否为空;
● orderBy、categoryId、keyword这三个参数,可传可不传;如果传的话,我们才会使用这个参数,把这个参数作为条件去查询;
● 所以,对于orderBy、categoryId、keyword这三个参数的判空:就不使用@Valid注解的Validation参数校验的方式,来判空了;
(3)如果传了keyword这个参数,比如传了“桃”,就表示我们搜索所有商品名中有“桃”的商品;对于这种模糊搜索的功能,我们使用%通配符和like关键字,去实现模糊查找;
● 在查找的时候,我们会使用【%通配符】和【like关键字】,在SQL中进行拼接,实现查找功能;
1.在ProductController类中,创建查询前台的商品列表的方法:list()方法;
/** * 前台的商品列表接口; * * @return */ @ApiOperation("前台商品列表") @GetMapping("/product/list") @ResponseBody public ApiRestResponse list(ProductListReq productListReq) { PageInfo pageInfoList = productService.list(productListReq); return ApiRestResponse.success(pageInfoList); }说明:
(1)因为前台的【商品列表】接口的参数很多,所以我们创建了一个实体类ProductListReq类,用这个实体类来接收参数;
ProductListReq类:
package com.imooc.mall.model.request; import io.swagger.models.auth.In; /** * 在开发【前台的商品列表】接口时,用该类的对象接收参数 */ public class ProductListReq { private String orderBy;//排序方式 private Integer categoryId;//商品分类Id private String keyword;//搜索关键词 private Integer pageNum = 1;//页数 private Integer pageSize = 10;//每页条数 public String getOrderBy() { return orderBy; } public void setOrderBy(String orderBy) { this.orderBy = orderBy; } public Integer getCategoryId() { return categoryId; } public void setCategoryId(Integer categoryId) { this.categoryId = categoryId; } public String getKeyword() { return keyword; } public void setKeyword(String keyword) { this.keyword = keyword; } public Integer getPageNum() { return pageNum; } public void setPageNum(Integer pageNum) { this.pageNum = pageNum; } public Integer getPageSize() { return pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } }● 根据前台的【商品列表】接口的参数,编写ProductListReq类,并且这儿给pageNum和pageSize设了默认值;
● 因为,接口是Get请求,即不是【Post请求,且把参数放在Body中】的情况;所以,这儿使用实体类接收参数的时候,是不能使用@RequestBody注解(这个注解是在,POST请求,且参数是放在Body中,而且后端我们使用实体类去接收参数时:才使用的)的;;;;然后,又因为我们这儿是使用实体类去接收参数,所以也不能使用@RequestParam注解;;;;;;;所以,这儿我们使用实体类去接收参数的时候,实体类的属性一定要和接口的参数名写一样,别写错了;
……………………………………………………
(2)请求方式,url,接收参数,要写对;
……………………………………………………
(3)因为根据接口文档中对接口返回数据格式的要求,我们需要使用PageHelper的PageInfo来包装分页数据,所以我们在开发Service层的方法时,其返回类型就是PageInfo格式的;
……………………………………………………
(4)Service层的list方法,在下一部分介绍;
2.1.在ProductServiceImpl类中,创建以分页的方式查询符合条件的商品的方法:list()方法;(重点!!!)
在ProductServiceImpl类中,创建查询前台的商品列表的方法:list()方法;
/** * 根据条件,以分页的方式,查询商品数据; * @param productListReq * @return */ @Override public PageInfo list(ProductListReq productListReq) { //先构建一个专门助力于查询的Query对象 ProductListQuery productListQuery = new ProductListQuery(); //搜索条件处理 //如果前端传了keyword这个参数;(其实,也就是前端搜索关键词了):就把这个条件赋值到productListQuery查询对象上去; if (!StringUtils.isEmpty(productListReq.getKeyword())) { //根据keyword拼凑"%keyword%",以好在数据库中进行模糊查询; String keyword = new StringBuilder().append("%").append(productListReq.getKeyword()).append("%").toString(); //然后,把这个在查询数据库时,可以直接使用的"%keyword%"查询条件,赋值到productListQuery查询对象上去; productListQuery.setKeyword(keyword); } //如果前端传了categoryId这个参数;(其实,也就是前端选择某个商品目录):就把这个条件赋值到productListQuery查询对象上去; if (productListReq.getCategoryId() != null) { //目录处理:如果查询某个目录下的商品,不仅要查询隶属于该目录下的商品,也要查询隶属于该目录的子目录下的商品; //所以,先调用以前编写的一个【递归查询所有子目录】的方法;获取当前目录和当前目录的所有子目录的递归查询结果; List<CategoryVO> categoryVOList = categoryService.listCategoryForCustomer(productListReq.getCategoryId()); //但是,上面的categoryVOList是一个嵌套的结果,我们要想获取当前目录和其子目录的CategoryId的集合,还需要做以下处理; //首先,创建一个List用来存放所有的CategoryId List<Integer> categoryIds = new ArrayList<>(); //很自然,当前传参的这个CategoryId肯定在需要添加到集合中; categoryIds.add(productListReq.getCategoryId()); //编写一个【遍历categoryVOList这种递归结构的数据,以获取所有categoryId的,工具方法】 getCategoryIds(categoryVOList, categoryIds); //然后,把这个在查询数据库时,【当前目录和当前目录所有子目录的categoryIds】的查询条件,赋值到productListQuery查询对象上去; productListQuery.setCategoryIds(categoryIds); } //排序条件的处理 //首先,尝试从productListReq这个传递参数中,获取排序的参数 String orderBy = productListReq.getOrderBy(); //如果我们从前端请求中的参数中,有orderBy这个有关排序的参数;;如果这个有关排序的参数,在我们预设的排序条件中的话; if (Constant.ProductListOrderBy.PRICE_ASC_DESC.contains(orderBy)) { PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize(), orderBy); } else { //如果前端没有传orderBy参数,或者,传递orderBy参数的值不符合我们在【Constant.ProductListOrderBy.PRICE_ASC_DESC】中定义的格式 PageHelper.startPage(productListReq.getPageNum(), productListReq.getPageSize()); } //调用Dao层编写的(可能有条件的)查询语句 List<Product> productList = productMapper.selectList(productListQuery); PageInfo pageInfo = new PageInfo(productList); return pageInfo; } /** * 工具方法,遍历【List<CategoryVO> categoryVOList】这种递归嵌套的数据结构,获取其中所有的categoryId; * @param categoryVOList * @param categoryIds */ private void getCategoryIds(List<CategoryVO> categoryVOList, List<Integer> categoryIds) { //遍历传过来的这个【递归嵌套接口的,CategoryVOList】 for (int i = 0; i < categoryVOList.size(); i++) { CategoryVO categoryVO = categoryVOList.get(i); if (categoryVO != null) { categoryIds.add(categoryVO.getId()); //递归调用 getCategoryIds(categoryVO.getChildCategory(), categoryIds); } } }说明:
(1.1)【categoryId,keyword】这两个查询条件处理:构建一个专门用于组织查询条件的Query对象:ProductListQuery类:用Query对象来组织查询条件,代码会更加有条理,也方便以后的扩展;(这个开发技巧,比较重要!!!)
● 因为,根据接口文档和项目的实际情况,可以看到前台的【商品列表】接口在查询商品时候,其查询条件可能存在以下几种情况:(PS:这儿的条件种类,其实还不算太多;;;;以后可能遇到条件种类很多的情况,到那时,专门构建一个Query对象的好处,会更加明显)
● 创建ProductListQuery类;
package com.imooc.mall.model.query; import java.util.List; /** * 描述:查询商品列表的Query */ public class ProductListQuery { private String keyword;//搜索关键词,这个条件 private List<Integer> categoryIds;//商品分类,这个条件 public String getKeyword() { return keyword; } public void setKeyword(String keyword) { this.keyword = keyword; } public List<Integer> getCategoryIds() { return categoryIds; } public void setCategoryIds(List<Integer> categoryIds) { this.categoryIds = categoryIds; } }说明:
(1)该Query类的第一个属性,就是keyword,其对应条件中的搜索关键词;
(2)该Query类的第二个属性,是一个List集合,这里面是:当接口传了categoryId这个商品分类的条件后:所有符合条件的商品的category_id;
也就是说:【如果在界面上,我们点击了“新鲜水果”后】→【就相当于我们传了categoryId=3,这个参数】→【就表示,我们此时加入了categoryId=3,这个搜索条件】→【这个条件经过处理,等到具体查product表的时候,就变成了:category_id在{3,4,11,12,14,19,28}的商品了】; 其实,这样一看,这个查询条件还是比较复杂的,由这儿也能感受到【专门构建查询条件的Query对象】的好处;
● 即,我们根据前端传过来的条件,把这些条件进行处理、分析,然后构建一个Query对象,然后再以这个Query对象作为条件,去数据库查询我们最终需要的结果;;;;这是一个十分不错的、条理清晰的策略;
……………………………………………………
(1.2)【categoryId,keyword】这两个查询条件处理:构建Query对象之:判断是否有【搜索关键词】这个条件:如果有,就把这个条件构建到Query对象上去;
● 这儿使用了Spring提供的一个判空工具:StringUtils.isEmpty();
● 因为,搜索关键词时:比如我们搜索“冰”,是要查询商品名中包含“冰”是商品;所以,在查数据库的时候,需要是模糊查找;所以,这儿,我们要先拼凑一个模糊查找要用的字符串;(PS:有关MySQL模糊查询,可以参考【数据库的基本查询三:【WHERE子句】条件查询;】);
……………………………………………………
(1.3)【categoryId,keyword】这两个查询条件处理:构建Query对象之:判断是否有【商品分类Id】这个条件:如果有,就把这个条件处理一下,然后构建到Query对象上去;
● 开发前台的【分类列表(递归)】接口时,创建的:递归查询分类目录的方法listCategoryForCustomer()方法回顾;如果忘记了,可以回去看下【Spring Boot电商项目30:商品分类模块九:前台的【分类列表(递归)】接口;】;
● listCategoryForCustomer()方法在此处使用的说明;
●【商品分类Id】这个条件的:处理流程分析;
由此,也可以发现,当接口传入了一个条件后,我们有的时候是无法直接用这个条件去查数据库;;;而是,要先处理下这个条件,把其处理成合适的东西,然后再那这个处理后的东西作为条件去查数据库; 具体来说,就是构建查询对象;;;这会使得程序结构更加条理清晰、有助于后期的功能扩展;
●工具方法:getCategoryIds()分析;这个方法很简单,没什么好说的;
……………………………………………………
至此,接口可能的条件:keyword和categoryId都已经构建到ProductListQuery这个Query对象上去了;
疑问:我们接口还有一个可能的条件就是排序方式,为什么这个条件不也构建到ProductListQuery这个Query对象上去呐?
这是因为,orderBy是有关排序的一个条件;我们这儿是使用PageHelper这个分页插件来实现分页查询;而,PageHelper在构建的时候,就可以设置排序方式,即我们可以通过PageHelper来应用这个条件;所以,就没有把其构建到ProductListQuery这个Query对象上去;
……………………………………………………
(2)【orderBy】这一个条件处理:在构建PageHelper的时候,应用这个排序条件;需要注意:这儿我们需要【在程序中,规定好有哪些字段可以排序】,这非常重要,有关程序的安全;
一种错误写法:前端传什么条件,我们就原样不动的把其传到MySQL上去进行排序;;;其实,有哪些字段可以排序,我们必须要提前设计好;
……………………………………………………
(3)以前面构建的Query对象作为条件,去调用我们在Dao层编写查询方法:selectList()方法;;这个方法,在下一部分介绍;
……………………………………………………
(4)把查询结果,包装成PageInfo对象,返回给调用方;
2.2.在ProductService接口中,反向生成方法的声明;
3.1在ProductMapper中,声明根据条件,查询商品的方法:selectList()方法;
说明:
(1)可以看到,虽然这儿我们传给Dao层的参数是一个实体类,但我们这儿依旧使用了@Param注解;
3.2在ProductMapper.xml中,编写对应的实现SQL;
<select id="selectList" parameterType="com.imooc.mall.model.query.ProductListQuery" resultMap="BaseResultMap"> select <include refid="Base_Column_List"/> from imooc_mall_product <where> <if test="query.keyword != null"> and name like #{query.keyword} </if> <if test="query.categoryIds != null"> and category_id in <foreach collection="query.categoryIds" close=")" item="item" open="(" separator=","> #{item} </foreach> </if> and status = 1 </where> order by update_time desc </select>说明:
(1)入参需要是我们的Query查询类:parameterType="com.imooc.mall.model.query.ProductListQuery";
(2)因为,这儿ProductListQuery中的两个条件是可有可无的;即,如果前端传了条件,这儿就有,如果前端没传条件,这儿就没有;所以,这儿查询SQL的时候,需要判断一下;于是就涉及到了Mybatis的动态SQL了;
● 有关mybatis动态SQL,如有需要可以参考:【MyBatis进阶二:MyBatis动态SQL;(仅仅涉及【if语句】)】;
(3)SQL语句,整体分析;
(4)这儿使用到了MySQL的like模糊查询;
● 有关MySQL的like模糊查询,如有需要可以参考:【数据库的基本查询三:【WHERE子句】条件查询;】;
(5)这儿使用到了MySQL的in子句,用来进行范围判断;
● 有关MySQL的in子句,如有需要可以参考:【数据库的基本查询三:【WHERE子句】条件查询;】;
(6)这儿使用到了Mybatis的<foreach>子标签;
● 上一次我们使用到Mybatis的<foreach>子标签是在【Spring Boot电商项目39:商品模块五:【批量上下架商品】接口;】
● Mybatis<foreach>子标签:最初,是在【MyBatis进阶八:Mybatis批处理;(批量插入,批量删除,批量更新待写…)】中进行介绍的;如有需要,可以去参考;
至此,前台的【商品列表】接口就开发完成了;
启动项目;
测试1:
……………………………………………………
测试2:
……………………………………………………
测试3:
……………………………………………………
测试4:
……………………………………………………
测试5:一个在目前的项目中,实际上不存在的情况;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。