这是多条件模糊搜索的前端界面。
可以看到,条件有五个。如果某项不选,就忽略这一项;五项都选,就查询同时满足这五项的数据。
- 姓名:搜索姓名中包含某个字符串的数据
- 订单状态:搜索订单状态相同于多个状态其中一个的数据(下拉选择框中是多选)
- 开始时间:搜索创建时间晚于某个时间的数据
- 截止时间:搜索创建时间早于某个时间的数据
- 地址:搜索地址中包含某个字符串的数据
数据类型
- 后台从搜索框里得到的
姓名、地址、开始与截止时间都是String类型, 订单状态是List[String, ...,String]类型,因为下拉选择框得到状态码的就是String类型。
操作类型
- 姓名和地址是模糊查询
- 订单状态需要再sql语句中循环列表做等价查询,
or连接 - 开始和截止时间是范围查询,如果开始时间和截止时间都有,就搜索处于其间的
我们知道,对于多条件模糊搜索,空值的处理是重点。这部分可以在前端就处理完,也可以在后端处理,也可以传到sql语句中再处理。我比较倾向于在后端规定好sql语句中需要的数据类型后,统统装载到一个HashMap里,把它送到sql中去处理,传参很方便,同时mybatis的sql语句支持 if、foreach 等操作,非常友好。
一、Controller层代码
@RequestMapping(value = "/search", method = RequestMethod.POST)
public String queryFuzzy(@RequestParam("s_name") String s_name,
@RequestParam(value = "s_order_status" ,required = false) List s_order_status, // 如果不加 required=false ,当为空时会报错
@RequestParam("s_startTime") String s_startTime,
@RequestParam("s_endTime") String s_endTime,
@RequestParam("s_address") String s_address,
@RequestParam(value = "page", defaultValue = "1") int page,
Model model
) throws ParseException {
// 处理可选的状态(List[String]类型 ————> List[String]类型)
ArrayList<Integer> codesInteger = new ArrayList<>(); // 这里不能初始化为null,否则下面的null.add()会报NLP的错误
if(s_order_status!=null && s_order_status.size()>0){ // 如果不先加null的判断,后面的null.size()会报NLP的错误
for(Object s : s_order_status) codesInteger.add(Integer.valueOf(String.valueOf(s))); // 将List <String>转换为List <Integer> 因为数据库中的状态码为Tinyint型,不能拿String去比较
}
// 处理日期(String类型 ————> Date类型)
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date startTime = null,endTime = null; // 处理空值
if(s_startTime!=null && s_startTime!=""){
startTime = formatter.parse(s_startTime);
}
if(s_endTime!=null && s_endTime!=""){
endTime = formatter.parse(s_endTime);
}
HashMap<String,Object> queryMap = new HashMap<>(); // key为String,value需要是为Object
queryMap.put("s_name",s_name);
queryMap.put("s_order_status",codesInteger);
queryMap.put("s_startTime",startTime);
queryMap.put("s_endTime",endTime);
queryMap.put("s_address",s_address);
List<Order> list = orderService.queryFuzzy(queryMap);
PageHelper.startPage(page, list.size()); // 不再分页,用一页来装完
model.addAttribute("msg", "查到了"+list.size()+"条信息");
model.addAttribute("pageInfo", new PageInfo<>(list));
return "views/customer/manage";
}
代码看起来比较多,但逻辑上很简单。首先,我前端数据是在bootstrap模态框中用form表单提交的,并且字段名和数据库中的不一致(因为用到了模态框,字段名要和主页面上的字段名区分开),所以我后端用@RequestParam 单个接收,毕竟查询条件也不多。需要注意的是,接收List列表时,设置了 required = false ,表示不对他做表单验证,这是必需的,否则会报错。
然后开始处理数据类型,首先就是List列表,列表中的每个数据都是字符串类型的状态码,不能拿它去和数据库中Integer类型的状态码去比较,所以需要把 List[String] 类型转换为 List[Integer] 类型。Java8中其实有一个现成的方法:
stringList.stream().map(Integer::parseInt).collect(Collectors.toList());
但我这里 parseInt 下面一直有红线,原因未知,遂放弃,改用了最笨的遍历方法。需要注意一下NPL的错误,在注释里有就不多说了。
另外就是日期类型,sql语句中日期比较只支持Date。而从前端接收到的是String类型。所以需要转换一下,同样需要注意烦人的NPL问题,也在注释里。
然后开辟一个 HashMap<String,Object>,去把所有处理好的查询条件put进去,拿它去经过Service层和Dao层到mapper里查询,得到查询结果list配合PageHelper返回给前端展示。
二、Mapper中的sql语句
重要的就是mapper里的sql语句。
<select id="queryFuzzy" parameterType="java.util.Map" resultType="cn.hsy.pojo.Order">
select * from my_order where 1=1
<if test="s_name!=null and s_name!=''">and name like concat('%',#{s_name},'%')</if>
<if test="s_address!=null and s_address!=''">and address like concat('%',#{s_address},'%')</if>
<if test="s_startTime!=null ">and create_time > #{s_startTime}</if>
<if test="s_endTime!=null ">and create_time < #{s_endTime}</if>
<if test="s_order_status!=null and s_order_status.size()>0">and (
<foreach collection="s_order_status" item="each" index="idx">
order_status=#{each}
<if test="idx != (s_order_status.size() - 1)">
or
</if>
</foreach>
)
</if>
</select>
分开来说,首先是
select * from my_order where 1=1
其中的 where 1=1 很重要,它是为了防止在无任何条件时,sql语句出错。具体来说:如果没有where 1=1 , 只在后面用 if 堆积判断条件,如果所有的 if 都不成立,那就相当于没有任何判断条件,上述sql语句就变成了select * from my_order where,可以看出where后的条件缺失,会导致sql语句报错。另外,where 1=1 是恒成立的,不会影响其它条件。
下面是姓名和地址的模糊查询
<if test="s_name!=null and s_name!=''">and name like concat('%',#{s_name},'%')</if>
<if test="s_address!=null and s_address!=''">and address like concat('%',#{s_address},'%')</if>
if test="" 是MyBatis中的判断语句,当test为true时,if标签内的语句会被执行,反之,则会被忽略。而 and 的作用是拼接多个判断语句,这个 and 就是把当前判断语句和前面的where 1=1 拼接起来。
name like concat('%',#{s_name},'%') 是模糊查询必需的写法,不能写为 name like {s_name} 。
下面是时间的范围查询,
<if test="s_startTime!=null ">and create_time > #{s_startTime}</if>
<if test="s_endTime!=null ">and create_time < #{s_endTime}</if>
同样用到了 if 来判空。比较早晚直接用小于大于号即可,但是在xml文件中,所有文本均会被解析器解析,使用 < 、>、& 符号是非法的,会被解析导致报错。为了避免错误,就需要就这些字符替换成实体引用。在xml中有5个预定义的实体引用:
| < | < | 小于号 |
|---|---|---|
| > | 大于号 | |
| & | & | 和号 |
| ' | ‘ | 单引号 |
| " | “ | 双引号 |
最后是状态的列表遍历等价查询。
<if test="s_order_status!=null and s_order_status.size()>0">and (
<foreach collection="s_order_status" item="each" index="idx">
order_status=#{each}
<if test="idx != (s_order_status.size() - 1)">
or
</if>
</foreach>
)
</if>
MyBatis支持 foreach 来实现遍历,在非空的情况下,先用 and 与上一个查询条件拼接。判断列表中的每一项时要用 or 拼接,虽说sql语句中 and 优先级已经比 or 要高了,但是为了可读性,我还是加了一对括号。
另外要注意查询完列表中最后一个元素后就不能再用 or 了,所以不能写成 order_status=#{each} or ,而要把 or 单拿出来,foreach 恰好也支持下标操作,就用 idx != (s_order_status.size() - 1) 来判断当前元素是否为最后一个元素,如果是,就不再加 or 了。
完毕。