1. MyBatis-Plus 自定义SQL实战解析
在真实业务场景中,我们经常会遇到需要构建复杂查询条件的情况。MyBatis-Plus(以下简称MP)提供的Wrapper机制确实能简化很多基础操作,但很多开发者容易陷入"必须完全使用Wrapper"的误区。实际上,MP官方文档明确建议:简单查询用Wrapper,复杂业务逻辑和核心数据变动应该回归Mapper XML。
1.1 Wrapper与自定义SQL的混合使用
先看一个典型场景:批量更新用户余额。假设我们需要对ID为1、2、3的用户各扣除200余额,传统做法可能是:
java复制void updateWithWrapper() {
List<Long> ids = List.of(1L, 2L, 3L);
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance=balance-200")
.in("id", ids);
userMapper.update(null, wrapper);
}
这种写法虽然能用,但存在两个问题:
- 扣除金额200被硬编码在Java代码中
- 当需要动态计算扣除金额时不够灵活
更优雅的做法是将SQL主体部分定义在Mapper中:
java复制// Service层
void deductWithCustomSql() {
List<Long> ids = List.of(1L, 2L, 3L);
Integer amount = 200;
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.in(User::getId, ids);
userMapper.updateBalanceByIds(wrapper, amount);
}
// Mapper接口
@Update("UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}")
void updateBalanceByIds(
@Param(Constants.WRAPPER) LambdaQueryWrapper<User> wrapper,
@Param("amount") Integer amount);
关键点说明:
${ew.customSqlSegment}会自动插入Wrapper生成的WHERE条件- 参数通过
@Param注解明确指定,避免SQL注入风险- 业务逻辑与SQL分离,更易于维护
1.2 复杂条件查询的最佳实践
当遇到多条件动态查询时,LambdaWrapper的优势就体现出来了:
java复制public List<User> searchUsers(String name, Integer status,
Integer minBalance, Integer maxBalance) {
return lambdaQuery()
.like(StringUtils.isNotBlank(name), User::getUsername, name)
.eq(status != null, User::getStatus, status)
.gt(minBalance != null, User::getBalance, minBalance)
.lt(maxBalance != null, User::getBalance, maxBalance)
.list();
}
这种链式写法不仅可读性强,而且每个条件都带有判空逻辑,避免生成不必要的WHERE条件。
2. MP Service层的深度应用
2.1 基础CRUD的优雅实现
MP的IService接口已经提供了绝大多数基础操作,正确使用可以大幅减少样板代码。典型的Service层结构如下:
java复制public interface UserService extends IService<User> {
// 自定义业务方法
void deductBalance(Long id, Integer money);
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
private final UserMapper userMapper;
@Override
public void deductBalance(Long id, Integer money) {
// 1. 查询用户
User user = getById(id);
if (user == null || user.getStatus() == 2) {
throw new BusinessException("用户状态异常");
}
// 2. 校验余额
if (user.getBalance() < money) {
throw new BusinessException("用户余额不足");
}
// 3. 执行扣减
userMapper.deductBalance(id, money);
}
}
注意事项:
- ServiceImpl泛型中第一个类型是对应的Mapper,第二个是实体类
- 基础CRUD方法如save/remove/update等可以直接使用
- 复杂业务操作应在Service中封装
2.2 批量操作的性能优化
MP提供了批量操作方法,但要真正发挥性能优势需要正确配置:
- MySQL需要在连接字符串中添加参数:
code复制jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true
- 批量插入示例:
java复制List<User> userList = new ArrayList<>();
// 添加大量用户数据...
userService.saveBatch(userList, 1000); // 每批1000条
实测表明,开启rewriteBatchedStatements后,批量插入性能可提升10倍以上。
3. 多表关联查询的解决方案
3.1 避免N+1查询问题
典型场景:查询用户及其地址信息。低效的做法是在循环中查询:
java复制// 反例:会产生N+1查询
List<User> users = userService.listByIds(ids);
for(User user : users) {
List<Address> addresses = addressMapper.selectByUserId(user.getId());
// ...
}
高效的做法是先批量查询,再在内存中组装:
java复制public List<UserVO> queryUserWithAddress(List<Long> ids) {
// 1. 批量查询用户
List<User> users = userService.listByIds(ids);
if (CollectionUtils.isEmpty(users)) {
return Collections.emptyList();
}
// 2. 批量查询地址
List<Long> userIds = users.stream()
.map(User::getId)
.collect(Collectors.toList());
List<Address> addresses = Db.lambdaQuery(Address.class)
.in(Address::getUserId, userIds)
.list();
// 3. 构建地址映射表
Map<Long, List<AddressVO>> addressMap = addresses.stream()
.map(addr -> BeanUtil.copyProperties(addr, AddressVO.class))
.collect(Collectors.groupingBy(AddressVO::getUserId));
// 4. 组装结果
return users.stream()
.map(user -> {
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
vo.setAddresses(addressMap.get(user.getId()));
return vo;
})
.collect(Collectors.toList());
}
3.2 使用Db静态工具解决循环依赖
当Service之间出现循环依赖时,可以使用MP的Db工具类直接操作Mapper:
java复制public UserVO queryUserAndAddress(Long id) {
User user = getById(id);
if (user == null) {
return null;
}
// 使用Db工具避免注入AddressService
List<Address> addresses = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, user.getId())
.list();
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
vo.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
return vo;
}
这种方法特别适合解决跨Service的查询需求,避免复杂的依赖关系。
4. 生产环境中的经验总结
4.1 常见问题排查
-
Wrapper条件不生效:
- 检查是否误用了
@TableField,字段名与数据库列名是否一致 - 确认Wrapper没有被后续操作覆盖
- 检查是否误用了
-
批量操作性能差:
- 确认MySQL连接参数配置正确
- 适当调整batchSize参数(通常500-1000为宜)
-
逻辑删除失效:
- 检查实体类是否添加
@TableLogic注解 - 确认全局配置与注解配置没有冲突
- 检查实体类是否添加
4.2 最佳实践建议
-
分层清晰:
- Controller:参数校验、DTO转换
- Service:业务逻辑、事务控制
- Mapper:SQL实现、复杂查询
-
SQL写法:
- 简单条件查询:使用LambdaWrapper
- 复杂关联查询:使用XML/注解SQL
- 批量操作:优先考虑MP提供的方法
-
性能优化:
- 关联查询避免N+1问题
- 大数据量操作使用批处理
- 合理使用二级缓存
在实际项目中,我通常会建立这样的目录结构:
code复制src/main/java
├── controller
├── service
│ ├── impl
├── mapper
│ ├── xml
├── entity
├── dto
│ ├── request
│ ├── response
└── config
这种结构保持了各层的独立性,又便于协作开发。特别是在处理复杂业务时,清晰的层次划分能让代码更易于维护。