1. MyBatis-Plus 自定义 SQL 与复杂查询概述
MyBatis-Plus 作为 MyBatis 的增强工具,在简化 CRUD 操作的同时,也提供了强大的条件构造器来处理复杂查询场景。在实际项目中,我们经常会遇到需要自定义 SQL 或构建复杂查询条件的情况,这正是 MyBatis-Plus 的 Wrapper 条件构造器大显身手的地方。
1.1 为什么需要自定义 SQL 和复杂查询
在日常开发中,简单的单表 CRUD 操作往往不能满足业务需求。我们可能需要:
- 多表关联查询
- 复杂的分组统计
- 动态条件拼接
- 子查询嵌套
- 特殊函数调用
MyBatis-Plus 的条件构造器(Wrapper)提供了一种面向对象的、类型安全的方式来构建这些复杂查询,避免了手动拼接 SQL 字符串带来的维护困难和 SQL 注入风险。
1.2 Wrapper 体系结构
MyBatis-Plus 提供了多种 Wrapper 实现:
- QueryWrapper:普通查询条件构造器
- LambdaQueryWrapper:Lambda 表达式形式的查询条件构造器
- UpdateWrapper:更新条件构造器
- LambdaUpdateWrapper:Lambda 表达式形式的更新条件构造器
Lambda 形式的 Wrapper 通过方法引用获取字段名,避免了硬编码字符串,提供了更好的类型安全和重构能力。
2. 基础条件构造方法详解
2.1 比较操作
MyBatis-Plus 提供了丰富的比较操作方法:
java复制// 等于
eq("name", "张三") // WHERE name = '张三'
ne("age", 18) // WHERE age != 18
// 大于小于
gt("score", 90) // WHERE score > 90
ge("score", 60) // WHERE score >= 60
lt("age", 30) // WHERE age < 30
le("age", 65) // WHERE age <= 65
// 区间
between("age", 18, 30) // WHERE age BETWEEN 18 AND 30
notBetween("id", 100, 200) // WHERE id NOT BETWEEN 100 AND 200
提示:对于数值型字段的比较操作,确保传入的参数类型与字段类型匹配,避免类型转换错误。
2.2 模糊查询
模糊查询是业务系统中常见的需求:
java复制like("name", "张") // WHERE name LIKE '%张%'
notLike("name", "李") // WHERE name NOT LIKE '%李%'
likeLeft("name", "先生") // WHERE name LIKE '%先生'
likeRight("name", "张") // WHERE name LIKE '张%'
2.3 集合操作
处理字段值在或不在某个集合中的情况:
java复制in("id", Arrays.asList(1, 2, 3)) // WHERE id IN (1, 2, 3)
notIn("role", Arrays.asList("admin", "super")) // WHERE role NOT IN ('admin', 'super')
// 使用子查询
inSql("dept_id", "SELECT id FROM dept WHERE status = 1")
// WHERE dept_id IN (SELECT id FROM dept WHERE status = 1)
2.4 空值处理
java复制isNull("description") // WHERE description IS NULL
isNotNull("phone") // WHERE phone IS NOT NULL
eqOrIsNull("name", null) // WHERE name IS NULL
3. 高级查询构造技巧
3.1 嵌套条件与逻辑组合
复杂查询往往需要组合多个条件:
java复制// AND 连接
and(wrapper -> wrapper.eq("dept_id", 10).gt("salary", 5000))
// WHERE (dept_id = 10 AND salary > 5000)
// OR 连接
or(wrapper -> wrapper.eq("status", 1).eq("status", 2))
// WHERE (status = 1 OR status = 2)
// 复杂组合
and(wrapper -> wrapper
.eq("type", "A")
.or()
.eq("level", 3)
.nested(nested -> nested.lt("age", 30).gt("score", 80)))
// WHERE (type = 'A' OR level = 3) AND (age < 30 AND score > 80)
3.2 排序与分组
java复制// 排序
orderByAsc("age", "create_time") // ORDER BY age ASC, create_time ASC
orderByDesc("score") // ORDER BY score DESC
// 分组
groupBy("dept_id", "gender") // GROUP BY dept_id, gender
having("avg(score) > {0}", 80) // HAVING avg(score) > 80
3.3 自定义 SQL 片段
对于无法通过条件构造器表达的复杂逻辑,可以使用自定义 SQL:
java复制apply("date_format(create_time,'%Y-%m-%d') = {0}", "2023-01-01")
// WHERE date_format(create_time,'%Y-%m-%d') = '2023-01-01'
// 直接拼接 SQL(谨慎使用)
last("FOR UPDATE") // 追加到 SQL 最后
警告:使用 apply 和 last 方法时,应避免直接将用户输入拼接到 SQL 中,防止 SQL 注入攻击。
4. 实战:复杂业务场景解决方案
4.1 动态条件查询
根据用户输入动态构建查询条件:
java复制public List<User> searchUsers(UserQuery query) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(query.getName())) {
wrapper.like(User::getName, query.getName());
}
if (query.getMinAge() != null) {
wrapper.ge(User::getAge, query.getMinAge());
}
if (query.getMaxAge() != null) {
wrapper.le(User::getAge, query.getMaxAge());
}
if (query.getDeptIds() != null && !query.getDeptIds().isEmpty()) {
wrapper.in(User::getDeptId, query.getDeptIds());
}
return userMapper.selectList(wrapper);
}
4.2 多表关联查询
虽然 MyBatis-Plus 主要面向单表操作,但也可以通过自定义 SQL 实现关联查询:
java复制@Select("SELECT u.*, d.name as dept_name FROM user u LEFT JOIN dept d ON u.dept_id = d.id ${ew.customSqlSegment}")
List<UserVO> selectUserWithDept(@Param(Constants.WRAPPER) Wrapper<User> wrapper);
// 使用方式
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1)
.like(User::getName, "张")
.orderByDesc(User::getCreateTime);
List<UserVO> users = userMapper.selectUserWithDept(wrapper);
4.3 子查询与 EXISTS
java复制// 使用 EXISTS
exists("SELECT 1 FROM user_role ur WHERE ur.user_id = id AND ur.role_id = 1")
// WHERE EXISTS (SELECT 1 FROM user_role ur WHERE ur.user_id = id AND ur.role_id = 1)
// 使用子查询作为条件
inSql("id", "SELECT user_id FROM user_role WHERE role_id = 2")
// WHERE id IN (SELECT user_id FROM user_role WHERE role_id = 2)
5. 性能优化与最佳实践
5.1 索引友好查询
- 避免在索引列上使用函数操作,如
apply("YEAR(create_time) = 2023") - 对于模糊查询,尽量使用
likeRight以便利用索引 - 合理使用
select()方法只查询需要的字段
5.2 分页优化
java复制// 使用 MyBatis-Plus 的分页插件
Page<User> page = new Page<>(1, 10);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1);
Page<User> result = userMapper.selectPage(page, wrapper);
5.3 批量操作
对于大批量数据操作,考虑使用批量方法:
java复制// 批量插入
List<User> users = ...;
userService.saveBatch(users);
// 批量更新
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(User::getStatus, 2)
.in(User::getId, ids);
userMapper.update(null, wrapper);
5.4 缓存策略
对于频繁查询但不常变化的数据,可以考虑添加缓存层:
java复制@Cacheable(value = "users", key = "#wrapper.cacheKey")
public List<User> findUsers(Wrapper<User> wrapper) {
return userMapper.selectList(wrapper);
}
6. 常见问题排查
6.1 条件不生效问题
- 检查是否错误使用了
condition参数 - 确认字段名是否正确(特别是使用字符串时)
- 检查是否有其他 Wrapper 覆盖了当前条件
6.2 SQL 语法错误
- 复杂 SQL 建议先在数据库客户端测试
- 检查自定义 SQL 片段中的特殊字符是否转义
- 确认数据库方言兼容性
6.3 性能问题
- 使用
EXPLAIN分析生成的 SQL 执行计划 - 检查是否缺少必要索引
- 避免全表扫描的操作
7. 扩展与进阶
7.1 自定义 Wrapper
对于特殊业务需求,可以继承 AbstractWrapper 实现自己的 Wrapper:
java复制public class MyWrapper<T> extends AbstractWrapper<T, String, MyWrapper<T>> {
// 自定义方法
public MyWrapper<T> myCondition(String column, Object value) {
addCondition(String.format("%s = my_func(%s)", column, value));
return typedThis;
}
}
7.2 动态表名
在多租户或分表场景下,可以使用动态表名:
java复制// 实现动态表名处理器
public class MyTableNameHandler implements TableNameHandler {
@Override
public String dynamicTableName(String sql, String tableName) {
return tableName + "_" + TenantContext.getCurrentTenant();
}
}
7.3 逻辑删除与乐观锁
MyBatis-Plus 内置支持:
java复制// 逻辑删除
@TableLogic
private Integer deleted;
// 乐观锁
@Version
private Integer version;
在实际项目中,合理运用 MyBatis-Plus 的条件构造器可以显著提高开发效率,同时保持代码的可维护性。对于特别复杂的查询场景,可以考虑结合原生 MyBatis 的 XML 映射文件使用,发挥各自优势。
