MyBatis-Plus(MP)的 Wrapper 是其最核心、最强大的功能之一,它提供了无 SQL 拼接的链式查询与更新能力,极大提升了开发效率。合理使用 Wrapper 能让代码更简洁、可读性更强,但也需注意性能与安全问题。


一、核心概念

1. 什么是 Wrapper?

Wrapper 是 MyBatis-Plus 提供的条件构造器,用于构建 SQL 的 WHEREORDER BYGROUP BY 等子句,无需手写 SQL

2. 主要实现类

类名 说明 使用场景
QueryWrapper<T> 查询条件构造器 SELECT 查询
UpdateWrapper<T> 更新条件构造器 UPDATE 操作
LambdaQueryWrapper<T> Lambda 风格查询构造器 类型安全,推荐使用
LambdaUpdateWrapper<T> Lambda 风格更新构造器 类型安全,推荐使用
ChainWrapper 链式操作包装器(如 lambdaQuery() 链式调用,更简洁

3. 核心优势

  • 无 SQL 拼接:避免字符串拼接 SQL,减少 SQL 注入风险。
  • 类型安全:使用 LambdaWrapper 可避免字段名写错。
  • 链式编程:代码流畅,可读性强。
  • 动态条件:支持 if-else 判断,自动忽略 null 或空值条件。

二、操作步骤(非常详细)

步骤 1:引入依赖(确保已集成 MP)

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.6</version>
</dependency>

步骤 2:定义实体类(使用 @TableField 注解)

@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    @TableField("user_name")
    private String userName;
    
    private Integer age;
    
    @TableField("email")
    private String email;
    
    @TableField("status")
    private Integer status;
}

步骤 3:定义 Mapper 接口

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

步骤 4:使用 LambdaQueryWrapper 进行查询(推荐)

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public List<User> getUsersByCondition(String name, Integer minAge, Integer status) {
        // 创建 LambdaQueryWrapper
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();

        // 条件拼接(自动忽略 null 值)
        wrapper.eq(StringUtils.hasText(name), User::getUserName, name)
               .ge(minAge != null, User::getAge, minAge)
               .eq(status != null, User::getStatus, status)
               .orderByDesc(User::getAge)
               .last("LIMIT 10"); // 谨慎使用,避免 SQL 注入

        return userMapper.selectList(wrapper);
    }
}

🔍 说明

  • eq(condition, column, value)conditiontrue 时才添加该条件。
  • ge:大于等于。
  • orderByDesc:按年龄降序。
  • last("LIMIT 10"):追加 SQL 片段(慎用)。

步骤 5:使用 LambdaUpdateWrapper 进行更新

public boolean updateUserStatus(Long userId, Integer newStatus) {
    LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
    
    wrapper.eq(User::getId, userId)
           .set(User::getStatus, newStatus)
           .set(User::getUpdateTime, new Date());

    return userMapper.update(null, wrapper) > 0;
}

⚠️ 注意:update(entity, wrapper)entitynull 表示只更新 wrapper.set() 的字段。


步骤 6:使用 QueryWrapper 查询字段子集(select)

public List<Map<String, Object>> getUserNames() {
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.select("user_name", "age") // 只查询指定字段
           .gt("age", 18);
    
    return userMapper.selectMaps(wrapper);
}

步骤 7:链式调用(ChainWrapper)

// 查询单个用户
User user = userMapper.selectOne(
    new LambdaQueryWrapper<User>()
        .eq(User::getUserName, "张三")
        .eq(User::getStatus, 1)
);

// 链式调用(MP 3.4+)
List<User> users = userMapper.lambdaQuery()
    .like(User::getUserName, "王")
    .gt(User::getAge, 20)
    .list();

三、常见错误与解决方案

错误现象 原因 解决方案
字段名映射错误 使用 QueryWrapper 时写错数据库字段名 改用 LambdaQueryWrapper 避免拼写错误
条件未生效 条件判断逻辑错误(如 == null 判断失败) 使用 eq(condition, ...) 动态控制
更新所有数据 UpdateWrapper 未加 where 条件 始终确保有 eqin 等限制条件
SQL 报错 last() 使用不当或特殊字符 避免 last(),或使用 apply() 安全传参
空指针异常 字段为 null 时未判断 使用 Objects.nonNull()StringUtils.hasText()

四、注意事项

  1. 避免 last() 被滥用

    • wrapper.last("LIMIT 1") 可能导致 SQL 注入。
    • 替代方案:使用 Page 分页。
  2. 不要忽略条件判断

    // ❌ 错误:未判断 null
    wrapper.eq(User::getAge, age); // age 为 null 时仍会拼接条件
    
    // ✅ 正确
    wrapper.ge(age != null, User::getAge, age);
    
  3. 更新操作慎用 update(entity, null)

    • 会更新所有字段,可能覆盖 null 值。
  4. Wrapper 不能跨方法复用

    • Wrapper 是一次性对象,使用后应丢弃。
  5. 复杂 SQL 仍建议写 XML

    • 如多表关联、子查询、窗口函数等。

五、使用技巧

1. 动态条件拼接(推荐模式)

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();

Optional.ofNullable(name).ifPresent(n -> wrapper.like(User::getUserName, n));
Optional.ofNullable(status).ifPresent(s -> wrapper.eq(User::getStatus, s));

2. 使用 apply() 安全嵌入 SQL 片段

wrapper.apply("date_format(create_time, '%Y-%m') = {0}", "2025-01");

{0} 会被安全参数化,防止 SQL 注入。

3. 分页查询(结合 Page)

Page<User> page = new Page<>(1, 10);
IPage<User> result = userMapper.selectPage(page, 
    new LambdaQueryWrapper<User>()
        .like(User::getUserName, "李")
);

4. 使用 inbetweenlike 等复杂条件

wrapper.in(User::getId, Arrays.asList(1L, 2L, 3L))
       .between(User::getAge, 18, 60)
       .likeRight(User::getUserName, "王"); // LIKE '王%'

六、最佳实践与性能优化

✅ 最佳实践

实践 说明
优先使用 LambdaWrapper 类型安全,避免字段名错误
动态条件使用 condition 参数 eq(condition, ...)
避免 last() 拼接 SQL 使用 apply() 或分页对象
更新时使用 UpdateWrapper 避免传入完整实体导致 null 覆盖
复杂查询回归 XML 保持 SQL 可读性与性能可控

⚡ 性能优化建议

  1. 只查所需字段

    wrapper.select(User::getUserName, User::getAge);
    
  2. 合理分页

    • 避免 LIMIT 10000, 10 深分页,可使用游标分页业务优化
  3. 避免全表扫描

    • 确保 WHERE 条件字段有索引。
  4. 缓存热点查询

    • 结合 Redis 缓存 Wrapper 查询结果。
  5. 批量操作使用 Service 批量方法

    service.saveBatch(list); // 批量插入
    

七、总结:Wrapper 使用决策树

是否简单 CRUD?
├── 是 → 使用 MP 自带方法(save, getById, removeById)
└── 否 → 是否有动态条件?
    ├── 是 → 使用 LambdaQueryWrapper / LambdaUpdateWrapper
    └── 否 → 是否复杂 SQL(多表、子查询)?
        ├── 是 → 回归 XML 写 SQL
        └── 否 → 可继续使用 Wrapper

八、安全提醒

  • 🔐 防止 SQL 注入:避免 wrapper.last("ORDER BY " + userInput)
  • 🛡️ 输入校验:对前端传入的查询参数进行合法性校验。
  • 📊 日志监控:开启 MP SQL 日志,监控异常查询。
# application.yml
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

结语

Wrapper 是 MyBatis-Plus 的“利器”,合理使用可提升开发效率 50% 以上。但切记:

“能力越大,责任越大” —— 避免滥用 last()、忽视条件判断、在复杂场景强行使用 Wrapper。

掌握 LambdaQueryWrapper + 动态条件 + 分页 + 类型安全,你就能在大多数场景下游刃有余,写出高效、安全、可维护的代码。