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() |
四、注意事项
避免
last()
被滥用:wrapper.last("LIMIT 1")
可能导致 SQL 注入。- 替代方案:使用
Page
分页。
不要忽略条件判断:
// ❌ 错误:未判断 null wrapper.eq(User::getAge, age); // age 为 null 时仍会拼接条件 // ✅ 正确 wrapper.ge(age != null, User::getAge, age);
更新操作慎用
update(entity, null)
:- 会更新所有字段,可能覆盖
null
值。
- 会更新所有字段,可能覆盖
Wrapper 不能跨方法复用:
Wrapper
是一次性对象,使用后应丢弃。
复杂 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 可读性与性能可控 |
⚡ 性能优化建议
只查所需字段:
wrapper.select(User::getUserName, User::getAge);
合理分页:
- 避免
LIMIT 10000, 10
深分页,可使用游标分页或业务优化。
- 避免
避免全表扫描:
- 确保
WHERE
条件字段有索引。
- 确保
缓存热点查询:
- 结合 Redis 缓存 Wrapper 查询结果。
批量操作使用
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
+ 动态条件 + 分页 + 类型安全,你就能在大多数场景下游刃有余,写出高效、安全、可维护的代码。