1. MyBatis条件查询实战指南
作为一名长期使用MyBatis进行数据库开发的工程师,我经常遇到各种复杂的查询场景。今天我想分享几种常见的条件查询方式及其优化方案,这些都是在实际项目中经过验证的有效方法。
1.1 OR条件与IN查询的转换
在查询条件中,我们经常会遇到需要匹配多个值的情况。比如查询某个设备属于多个车间中的任意一个,传统做法是使用OR条件:
java复制queryWrapper.and(!CollectionUtils.isEmpty(req.getWorkshopCodes()), wrapper -> {
for (String paramType : req.getWorkshopCodes()) {
wrapper.or().eq(DeviceShiftInfo::getWorkshopCode, paramType);
}
});
这种方式生成的SQL是:
sql复制(workshop_code = '测试1' OR workshop_code = '测试2')
但实际上,这种场景更适合使用IN查询,代码会更简洁:
java复制queryWrapper.in(!CollectionUtils.isEmpty(req.getWorkshopCodes()),
DeviceShiftInfo::getWorkshopCode,
req.getWorkshopCodes());
生成的SQL也更高效:
sql复制workshop_code IN('测试1','测试2')
提示:IN查询在大多数数据库引擎中都比多个OR条件性能更好,特别是当值列表较长时。MySQL 5.7+对IN查询有专门优化。
1.2 逗号分隔字符串的查询
很多老系统会使用逗号分隔的字符串存储多个值,比如"测试1,测试2"。查询这种字段时,我们需要使用FIND_IN_SET函数:
java复制queryWrapper.and(!CollectionUtils.isEmpty(req.getWorkshopCodes()), wrapper -> {
for (String paramType : req.getWorkshopCodes()) {
wrapper.or()
.apply("FIND_IN_SET({0}, workshop_code)", paramType);
}
});
对应的SQL是:
sql复制WHERE ((FIND_IN_SET('1', workshop_code) OR FIND_IN_SET('2', workshop_code)))
注意:FIND_IN_SET虽然方便,但有性能问题。如果数据量大,建议考虑改造数据库设计,使用关联表或JSON类型。
1.3 模糊查询的实现
模糊查询是另一个常见需求,比如要查询车间代码包含某些字符的设备:
java复制queryWrapper.and(!CollectionUtils.isEmpty(req.getWorkshopCodes()), wrapper -> {
for (String paramType : req.getWorkshopCodes()) {
wrapper.or().like(DeviceShiftInfo::getWorkshopCode, paramType);
}
});
生成的SQL是:
sql复制WHERE ((workshop_code LIKE '%1%' OR workshop_code LIKE '%2%'))
提示:前导通配符(%在前)会导致索引失效,如果性能要求高,考虑使用全文索引或其他搜索方案。
2. 高级查询技巧
2.1 JSON数组的查询
现代数据库如MySQL 8.0+支持JSON类型,我们可以存储数组并查询:
sql复制WHERE ((JSON_OVERLAPS(remark, '["1","2"]')))
JSON_OVERLAPS函数会检查两个JSON数组是否有交集,只要数据库字段的数组包含查询集合中的任意一个值,记录就会被返回。
在MyBatis-Plus中可以这样使用:
java复制queryWrapper.apply("JSON_OVERLAPS(remark, {0})",
JsonUtils.toJsonString(paramList));
2.2 动态条件组合
实际项目中,查询条件往往是动态组合的。MyBatis-Plus的Wrapper提供了灵活的条件构建方式:
java复制LambdaQueryWrapper<DeviceShiftInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DeviceShiftInfo::getStatus, "active");
if (!CollectionUtils.isEmpty(req.getWorkshopCodes())) {
wrapper.and(w -> {
for (String code : req.getWorkshopCodes()) {
w.or().eq(DeviceShiftInfo::getWorkshopCode, code);
}
});
}
if (StringUtils.isNotBlank(req.getKeyword())) {
wrapper.and(w -> w.like(DeviceShiftInfo::getDeviceName, req.getKeyword())
.or()
.like(DeviceShiftInfo::getDeviceCode, req.getKeyword()));
}
这种写法可以灵活应对各种查询场景,同时保持代码清晰。
3. 性能优化建议
3.1 索引设计原则
针对不同的查询方式,索引设计也有差异:
- 对于等值查询(=或IN),单列索引即可
- 对于LIKE查询,只有后缀通配符(如'abc%')能使用索引
- JSON列查询需要MySQL 8.0+的函数索引支持
3.2 查询方式选择指南
根据数据存储格式选择最优查询方式:
| 存储格式 | 推荐查询方式 | 适用版本 | 性能评价 |
|---|---|---|---|
| 单值字段 | IN查询 | 所有版本 | ★★★★★ |
| 逗号分隔 | FIND_IN_SET | 所有版本 | ★★☆☆☆ |
| JSON数组 | JSON_OVERLAPS | MySQL 8.0+ | ★★★★☆ |
| 模糊匹配 | LIKE(慎用) | 所有版本 | ★☆☆☆☆ |
3.3 分页查询优化
当数据量大时,分页查询需要特别注意:
java复制// 不好的写法 - 深分页性能差
Page<DeviceShiftInfo> page = new Page<>(10000, 10);
deviceShiftInfoMapper.selectPage(page, wrapper);
// 优化写法 - 使用游标分页
wrapper.last("LIMIT 10 OFFSET 10000");
对于超大数据集,考虑使用基于ID的范围分页:
java复制wrapper.gt(DeviceShiftInfo::getId, lastId)
.orderByAsc(DeviceShiftInfo::getId)
.last("LIMIT 10");
4. 常见问题排查
4.1 SQL注入防范
使用MyBatis-Plus的条件构造器时,大部分情况下可以避免SQL注入。但使用apply方法直接写SQL片段时要特别注意:
java复制// 不安全的写法
wrapper.apply("FIND_IN_SET(" + param + ", workshop_code)");
// 安全的写法
wrapper.apply("FIND_IN_SET({0}, workshop_code)", param);
4.2 空集合处理
当传入的集合可能为空时,需要特别注意:
java复制// 不安全的写法 - 空集合会导致SQL语法错误
wrapper.in(DeviceShiftInfo::getWorkshopCode, workshopCodes);
// 安全的写法
if (!CollectionUtils.isEmpty(workshopCodes)) {
wrapper.in(DeviceShiftInfo::getWorkshopCode, workshopCodes);
} else {
// 处理空集合情况,如返回空结果或忽略该条件
return Collections.emptyList();
}
4.3 多条件优先级
复杂的条件组合时,要注意括号的使用:
java复制// 条件A AND (条件B OR 条件C)
wrapper.eq("A", valueA)
.and(w -> w.eq("B", valueB).or().eq("C", valueC));
// 错误写法 - 优先级不对
wrapper.eq("A", valueA)
.eq("B", valueB).or().eq("C", valueC);
5. 实际案例分享
最近我们项目中有一个需求:查询属于特定几个车间、状态为活跃、且设备名称包含关键字的设备。最终实现如下:
java复制public PageResult<DeviceShiftInfo> queryDevices(DeviceQueryReq req) {
LambdaQueryWrapper<DeviceShiftInfo> wrapper = new LambdaQueryWrapper<>();
// 基础条件
wrapper.eq(DeviceShiftInfo::getIsActive, true);
// 车间条件
if (!CollectionUtils.isEmpty(req.getWorkshopCodes())) {
if (isJsonArrayStored(req.getWorkshopFieldType())) {
// JSON数组存储方式
wrapper.apply("JSON_OVERLAPS(workshop_code, {0})",
JsonUtils.toJsonString(req.getWorkshopCodes()));
} else if (isCommaSeparated(req.getWorkshopFieldType())) {
// 逗号分隔存储方式
wrapper.and(w -> {
for (String code : req.getWorkshopCodes()) {
w.or().apply("FIND_IN_SET({0}, workshop_code)", code);
}
});
} else {
// 普通单值存储
wrapper.in(DeviceShiftInfo::getWorkshopCode, req.getWorkshopCodes());
}
}
// 关键字搜索
if (StringUtils.isNotBlank(req.getKeyword())) {
wrapper.and(w -> w.like(DeviceShiftInfo::getDeviceName, req.getKeyword())
.or()
.like(DeviceShiftInfo::getDeviceCode, req.getKeyword()));
}
// 分页查询
Page<DeviceShiftInfo> page = new Page<>(req.getPageNum(), req.getPageSize());
IPage<DeviceShiftInfo> result = deviceShiftInfoMapper.selectPage(page, wrapper);
return new PageResult<>(result.getRecords(), result.getTotal());
}
这个实现考虑了多种存储格式,并且保证了查询的高效性。在实际使用中,我们通过数据库的EXPLAIN分析,进一步优化了索引设计,使查询时间从最初的800ms降低到了50ms左右。