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


一、核心概念

1. 什么是 Wrapper?

Wrapper 是 MyBatis-Plus 提供的条件构造器,用于构建 SQL 的 WHERE、ORDER BY、GROUP 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):condition 为 true 时才添加该条件。
  • 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) 中 entity 为 null 表示只更新 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 条件 始终确保有 eq、in 等限制条件
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. 使用 in、between、like 等复杂条件

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 + 动态条件 + 分页 + 类型安全,你就能在大多数场景下游刃有余,写出高效、安全、可维护的代码。