在数据持久层开发中,动态SQL的构建一直是影响开发效率的关键因素。传统MyBatis虽然提供了XML和注解两种方式编写SQL,但在处理复杂条件查询时,开发者往往需要编写大量重复的判断逻辑。MyBatis-Plus作为MyBatis的增强工具,其动态SQL能力通过Lambda表达式和Wrapper类的组合,实现了类型安全的条件构造。
我经历过一个电商后台管理系统开发项目,商品列表查询接口需要支持多达12个筛选条件。最初使用原生MyBatis时,XML文件中充斥着大量的<if test="...">标签,单个查询语句就超过200行。改用MyBatis-Plus的动态SQL方案后,同样的查询逻辑代码量减少了60%,且编译时就能发现参数类型错误,这在大型项目中尤为重要。
MyBatis-Plus提供了AbstractWrapper抽象类作为所有条件构造器的基类,其核心子类包括:
在实际项目中,我推荐优先使用Lambda变体,因为它们提供了编译时类型检查。例如构建用户查询条件时:
java复制LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1)
.between(User::getCreateTime, startDate, endDate)
.likeRight(User::getName, "张");
这种写法相比字段名字符串有三大优势:
MyBatis-Plus提供了丰富的条件构造方法,以下是最常用的几类:
=<>>>=<<=特殊场景下可以使用apply方法实现自定义比较逻辑:
java复制wrapper.apply("date_format(create_time,'%Y-%m') = {0}", "2023-06");
对于大列表IN查询要注意:
当IN列表参数超过1000个时,某些数据库会报错。解决方案是分批次查询,或者改用临时表JOIN方式。
%value%NOT LIKE '%value%'%valuevalue%模糊查询性能优化建议:
复杂查询场景往往需要组合多个条件组。例如查询VIP用户或者普通用户中消费金额大于1000的:
java复制wrapper.and(w -> w.eq(User::getVipLevel, 1)
.or()
.eq(User::getVipLevel, 0).gt(User::getConsumption, 1000))
.orderByDesc(User::getCreateTime);
对应的SQL输出:
sql复制WHERE (vip_level = 1 OR (vip_level = 0 AND consumption > 1000))
ORDER BY create_time DESC
实际业务中经常需要根据参数动态添加条件。推荐使用以下模式:
java复制public Page<User> queryUsers(UserQuery query) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 基础条件
wrapper.eq(User::getDeleted, 0);
// 动态条件
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());
}
return userMapper.selectPage(query.toPage(), wrapper);
}
对于特别复杂的查询条件,可以将部分逻辑提取为SQL片段:
java复制@SelectProvider(type = UserSqlProvider.class, method = "selectComplexUsers")
List<User> selectComplexUsers(@Param("ew") Wrapper<User> wrapper);
// 在Provider类中
public String selectComplexUsers(Wrapper<User> wrapper) {
String sql = "SELECT * FROM user " +
SqlUtils.getWrapperCondition(wrapper);
// 可以继续追加复杂逻辑
sql += " AND EXISTS (SELECT 1 FROM order WHERE user_id = user.id)";
return sql;
}
动态SQL构造必须考虑索引使用效率:
WHERE a=1 AND b>10 优于 WHERE b>10 AND a=1!=或<>操作符当需要处理大量数据时,建议:
selectObjs只查询必要字段last("LIMIT x,y")而非MP自带分页java复制try (Cursor<User> cursor = userMapper.selectCursor(wrapper)) {
cursor.forEach(user -> process(user));
}
启用逻辑删除后(@TableLogic),所有查询会自动附加删除条件。需要特别注意:
wrapper.ignoreLogicDel()可临时忽略逻辑删除对于需要联表查询的场景,推荐方案:
java复制@Select("SELECT u.*, d.name AS deptName FROM user u LEFT JOIN department d ON u.dept_id = d.id ${ew.customSqlSegment}")
List<UserVO> selectUserWithDept(@Param("ew") Wrapper<User> wrapper);
安全处理前端传入的排序字段:
java复制public void applyOrder(Wrapper<User> wrapper, String orderField, Boolean isAsc) {
try {
Field field = User.class.getDeclaredField(orderField);
if (isAsc) {
wrapper.orderByAsc(User::getField);
} else {
wrapper.orderByDesc(User::getField);
}
} catch (NoSuchFieldException e) {
// 默认排序
wrapper.orderByDesc(User::getId);
}
}
在多租户系统中,可以通过自动注入租户条件实现数据隔离:
java复制public class MyTenantHandler implements TenantHandler {
@Override
public Expression getTenantId() {
return new LongValue(UserContext.getTenantId());
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean doTableFilter(String tableName) {
// 过滤不需要租户隔离的表
return !"user".equalsIgnoreCase(tableName);
}
}
新手常犯的错误是直接传递null值:
java复制wrapper.eq(User::getType, query.getType()); // 当type为null时会产生WHERE type=null
正确做法是:
java复制if (query.getType() != null) {
wrapper.eq(User::getType, query.getType());
}
处理日期类型时要考虑时区问题:
java复制wrapper.between(User::getCreateTime,
LocalDateTime.of(2023,1,1,0,0).atZone(ZoneId.systemDefault()).toInstant(),
LocalDateTime.of(2023,12,31,23,59).atZone(ZoneId.systemDefault()).toInstant());
虽然Wrapper能防止大部分注入,但在使用apply或last时要特别注意:
java复制// 危险写法
wrapper.apply("column = " + userInput);
// 安全写法
wrapper.apply("column = {0}", userInput);
只更新非空字段:
java复制public void updateSelective(User user) {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<User>()
.eq(User::getId, user.getId());
if (user.getName() != null) {
wrapper.set(User::getName, user.getName());
}
if (user.getAge() != null) {
wrapper.set(User::getAge, user.getAge());
}
userMapper.update(null, wrapper);
}
使用executeBatch提升批量操作性能:
java复制List<User> users = ...;
SqlHelper.executeBatch(User.class, log, users, (sqlSession, entity) -> {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.updateById(entity);
});
实现回收站恢复功能:
java复制public void restore(Long id) {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<User>()
.eq(User::getId, id)
.set(User::getDeleted, 0);
userMapper.update(null, wrapper);
}
在大型项目中,我们通过AOP实现了统一的查询条件处理,自动注入组织架构权限过滤等通用条件。MyBatis-Plus的动态SQL能力结合自定义扩展,可以构建出既灵活又规范的数据访问层。经过多个项目的实践验证,合理使用Wrapper能减少30%以上的DAO层代码量,同时显著降低SQL错误率。