彻底告别原生SQL:MyBatis-Plus条件构造器实战手册

在Java持久层开发中,我们常常陷入原生SQL的泥潭——字符串拼接容易出错、参数绑定存在注入风险、复杂条件难以维护。MyBatis-Plus的条件构造器(QueryWrapper/UpdateWrapper)正是为解决这些问题而生。本文将带你从实战角度掌握这些工具,让你的数据库操作既安全又优雅。

1. 为什么需要条件构造器

想象这样一个场景:你需要根据前端传入的10个可选参数动态构建查询条件。如果用传统方式,代码中会出现大量 if(param != null) 的判断和字符串拼接,不仅难以维护,还可能引发SQL注入漏洞。这正是条件构造器要解决的核心问题。

原生SQL拼接的三大痛点:

  • 安全风险 :手动拼接容易导致SQL注入
  • 维护困难 :条件变更需要修改字符串逻辑
  • 类型不安全 :字段名硬编码,重构时易出错

对比示例:

// 原生SQL方式(危险!)
String sql = "SELECT * FROM user WHERE 1=1";
if(name != null) {
    sql += " AND name = '" + name + "'";  // 直接拼接有注入风险
}

// QueryWrapper方式(安全)
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq(name != null, "name", name);  // 自动参数化处理

2. QueryWrapper核心技巧

2.1 基础条件构建

QueryWrapper提供了丰富的条件方法,覆盖了SQL中的各种操作:

QueryWrapper<User> qw = new QueryWrapper<>();
qw.eq("status", 1)              // WHERE status = 1
  .gt("create_time", startDate)  // AND create_time > ?
  .le("age", 30)                // AND age <= 30
  .like("username", "admin")    // AND username LIKE '%admin%'
  .in("id", Arrays.asList(1,2,3)) // AND id IN (1,2,3)
  .orderByDesc("create_time");  // ORDER BY create_time DESC

特别提醒 :对于可能为null的参数,使用条件方法的重载版本:

// 只有当enable不为null时才会添加条件
qw.eq(enable != null, "enable", enable);

2.2 动态条件组合

复杂查询场景下,我们需要灵活组合AND/OR条件:

qw.nested(i -> i.eq("type", 1).or().eq("type", 2))
  .and(j -> j.gt("score", 90).or().lt("score", 60));

生成的SQL:

WHERE (type = 1 OR type = 2) AND (score > 90 OR score < 60)

2.3 Lambda表达式最佳实践

为避免字段名的硬编码,推荐使用Lambda方式:

LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.eq(User::getDepartment, "研发部")
   .between(User::getCreateTime, startDate, endDate);

优势:

  • 编译时检查字段名
  • 重构友好
  • 智能提示支持

3. UpdateWrapper高级用法

3.1 条件更新模式

UpdateWrapper可以同时设置更新值和更新条件:

UpdateWrapper<User> uw = new UpdateWrapper<>();
uw.set("login_count", 0)          // SET login_count = 0
  .setSql("version = version + 1") // 原生SQL片段
  .eq("id", userId);              // WHERE id = ?

3.2 增量更新技巧

实现原子性更新操作:

uw.setSql("balance = balance + " + amount)  // 原子增加
  .eq("id", accountId);

3.3 乐观锁集成

结合@Version注解实现乐观锁:

@Version
private Integer version;

// 更新时会自动带上version条件
uw.eq("version", oldVersion)
  .set("name", newName);

4. 实战避坑指南

4.1 NULL值处理陷阱

错误示范

qw.eq("name", null);  // 会生成 name = null,不符合SQL语义

正确做法

qw.isNull("name");    // 生成 name IS NULL

4.2 索引失效场景

以下写法会导致索引失效:

qw.apply("DATE(create_time) = '2023-01-01'");  // 对字段使用函数

应改为:

qw.between("create_time", "2023-01-01 00:00:00", "2023-01-01 23:59:59");

4.3 性能优化建议

  1. 避免在循环中创建Wrapper
  2. 复杂查询考虑使用 @Select 注解+XML方式
  3. 大批量更新使用专用方法:
// 比updateWrapper更高效
userMapper.update(null, 
    new UpdateWrapper<User>()
        .set("status", 0)
        .lt("last_login", LocalDate.now().minusMonths(6)));

5. 扩展应用场景

5.1 多表关联查询

虽然Wrapper主要针对单表,但可以结合自定义SQL实现关联查询:

@Select("SELECT u.*, d.name as dept_name FROM user u LEFT JOIN department d ON u.dept_id = d.id ${ew.customSqlSegment}")
List<User> selectUserWithDept(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper);

5.2 动态字段处理

通过反射实现动态字段选择:

public <T> QueryWrapper<T> buildWrapper(Class<T> clazz, Map<String, Object> params) {
    QueryWrapper<T> wrapper = new QueryWrapper<>();
    params.forEach((field, value) -> {
        if(value != null) {
            wrapper.eq(StringUtils.camelToUnderline(field), value);
        }
    });
    return wrapper;
}

在实际项目中,我们团队通过全面采用Wrapper替代原生SQL,使SQL相关Bug减少了70%,代码可读性显著提升。特别是在复杂业务场景下,条件构造器的链式调用比分散的字符串拼接更易于维护。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐