作为一名长期使用 MyBatis-Plus 的开发老兵,我经常看到新手在遇到复杂查询需求时手足无措。虽然 MyBatis-Plus 的 BaseMapper 提供了强大的 CRUD 能力,但真实业务场景往往需要更灵活的 SQL 控制。今天我就来分享几种实用的自定义 SQL 方案,这些都是在实际项目中经过验证的可靠方法。
在订单系统中,我们可能需要根据动态条件查询用户历史订单;在报表统计时,经常要处理多表关联和复杂聚合计算。这些场景下,自动生成的 SQL 往往力不从心。MyBatis-Plus 的自定义 SQL 功能就像给你的瑞士军刀加上了专业工具头,既保留了便捷性,又提供了专业级的灵活性。
注解方式最适合快速实现简单查询。比如用户模块中,我们经常需要根据手机号查找用户:
java复制@Select("SELECT * FROM user WHERE phone = #{phone} AND is_deleted = 0")
User findByPhone(@Param("phone") String phone);
注意:一定要加上 @Param 注解明确参数名,这是 MyBatis 的要求,避免参数绑定错误
当查询条件动态变化时,@SelectProvider 就派上用场了。比如商品搜索功能:
java复制@SelectProvider(type = ProductSqlProvider.class, method = "searchProducts")
List<Product> searchProducts(ProductQuery query);
class ProductSqlProvider {
public String searchProducts(ProductQuery query) {
return new SQL() {{
SELECT("*");
FROM("product");
if (query.getCategoryId() != null) {
WHERE("category_id = #{query.categoryId}");
}
if (StringUtils.isNotBlank(query.getKeyword())) {
WHERE("name LIKE CONCAT('%', #{query.keyword}, '%')");
}
if (query.getMinPrice() != null) {
WHERE("price >= #{query.minPrice}");
}
ORDER_BY("create_time DESC");
}}.toString();
}
}
这里使用了 MyBatis 的 SQL 构建器,比手动拼接字符串更安全可靠。我在电商项目中用这种方式处理了包含 10+ 条件的商品搜索,代码依然保持清晰。
报表统计是 XML 方式大显身手的场景。比如我们需要统计每个部门的用户数量:
xml复制<select id="countUsersByDepartment" resultType="map">
SELECT
d.name as departmentName,
COUNT(u.id) as userCount
FROM department d
LEFT JOIN user u ON d.id = u.department_id
GROUP BY d.id
</select>
XML 的动态 SQL 标签让复杂条件查询变得优雅。比如这个订单筛选案例:
xml复制<select id="searchOrders" resultType="OrderVO">
SELECT o.*, u.username
FROM orders o
JOIN user u ON o.user_id = u.id
<where>
<if test="status != null">
AND o.status = #{status}
</if>
<if test="startTime != null">
AND o.create_time >= #{startTime}
</if>
<if test="endTime != null">
AND o.create_time <= #{endTime}
</if>
<if test="userIds != null and userIds.size() > 0">
AND o.user_id IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</if>
</where>
ORDER BY o.create_time DESC
</select>
提示:XML 中的特殊字符如 < 需要用 < 转义,这是新手常踩的坑
查询构造器特别适合在 Service 层动态构建查询条件。比如这个权限检查场景:
java复制public List<Menu> getUserMenus(Long userId) {
QueryWrapper<Menu> wrapper = new QueryWrapper<>();
wrapper.inSql("id",
"SELECT menu_id FROM role_menu WHERE role_id IN (" +
"SELECT role_id FROM user_role WHERE user_id = " + userId + ")");
wrapper.eq("is_show", 1);
wrapper.orderByAsc("sort");
return menuMapper.selectList(wrapper);
}
Java8 用户可以使用 LambdaQueryWrapper 获得类型安全:
java复制public List<User> findVIPUsers(LocalDate registerDate) {
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.ge(User::getRegisterDate, registerDate)
.eq(User::getVipLevel, 1)
.orderByDesc(User::getConsumeAmount);
return userMapper.selectList(wrapper);
}
MyBatis-Plus 的分页插件使用非常简单:
java复制public Page<User> searchUsers(int pageNum, int pageSize, String keyword) {
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
if (StringUtils.isNotBlank(keyword)) {
wrapper.like(User::getUsername, keyword);
}
return userMapper.selectPage(page, wrapper);
}
当需要复杂分页查询时,可以这样处理:
xml复制<select id="selectUserPage" resultType="UserVO">
SELECT u.*, d.name as deptName
FROM user u LEFT JOIN department d ON u.dept_id = d.id
${ew.customSqlSegment}
</select>
对应的 Java 代码:
java复制public Page<UserVO> searchUserPage(Page<UserVO> page, UserQuery query) {
QueryWrapper<UserVO> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(query.getName()), "u.username", query.getName())
.eq(query.getDeptId() != null, "u.dept_id", query.getDeptId());
return userMapper.selectUserPage(page, wrapper);
}
在多对多关系中,警惕 N+1 查询。比如查询用户及其角色:
java复制// 错误示范:会导致 N+1 查询
List<User> users = userMapper.selectList(null);
users.forEach(user -> {
List<Role> roles = roleMapper.selectByUserId(user.getId());
user.setRoles(roles);
});
// 正确做法:使用 JOIN 一次查询
@Select("SELECT u.*, r.id as role_id, r.name as role_name " +
"FROM user u LEFT JOIN user_role ur ON u.id = ur.user_id " +
"LEFT JOIN role r ON ur.role_id = r.id")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "roles", column = "role_id",
many = @Many(select = "com.example.mapper.RoleMapper.selectById"))
})
List<User> selectUsersWithRoles();
动态查询时要注意索引使用:
java复制// 会导致索引失效的写法
wrapper.apply("DATE_FORMAT(create_time,'%Y-%m-%d') = '2023-01-01'");
// 正确的写法
wrapper.between("create_time",
LocalDateTime.of(2023, 1, 1, 0, 0),
LocalDateTime.of(2023, 1, 1, 23, 59, 59));
MyBatis-Plus 的 saveBatch 默认是单条插入,性能较差。可以这样优化:
java复制@Insert("<script>" +
"INSERT INTO user (name, age) VALUES " +
"<foreach collection='list' item='item' separator=','>" +
"(#{item.name}, #{item.age})" +
"</foreach>" +
"</script>")
void batchInsert(@Param("list") List<User> users);
商品扣库存的乐观锁实现:
java复制@Update("UPDATE product SET stock = stock - #{num}, version = version + 1 " +
"WHERE id = #{id} AND version = #{version} AND stock >= #{num}")
int deductStock(@Param("id") Long id,
@Param("num") Integer num,
@Param("version") Integer version);
经过多个项目实践,我总结出这样的选择策略:
在微服务架构中,我倾向于将简单查询放在 Service 层使用 QueryWrapper,复杂查询放在 Mapper 层使用 XML,这样职责划分更清晰。