乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

@SelectProvider

导学

本节学习目标:

  • 掌握 @SelectProvider 注解的核心属性,能够通过 Provider 类动态生成 SQL
  • 理解 type 和 method 属性的作用,学会编写完整的 Provider 类
  • 了解 MyBatis 3.5.1+ 的 ProviderMethodResolver 接口,知道如何省略 method 属性
  • 简要了解 @InsertProvider、@UpdateProvider、@DeleteProvider 的存在与用法

定义

@SelectProvider 是 MyBatis 提供的动态 SQL 注解,标注在 Mapper 接口的方法上,用于指定一个 Provider 类及其方法,由该方法在运行时动态生成 SQL 字符串。它对应 XML 映射文件中的动态 SQL 标签(如 <if>、<where>、<foreach> 等)。

痛点解决:在注解开发中,如果查询条件动态变化(如用户可能只输入姓名、只输入年龄、或两者都输入),静态的 @Select 字符串无法满足需求。@SelectProvider 将 SQL 的构建逻辑转移到 Java 代码中,通过条件判断动态拼接 WHERE 子句,使注解开发也能优雅地处理复杂动态 SQL。

注解方式 vs XML 方式对比

对比维度@SelectProvider 注解方式XML 动态 SQL 方式
SQL 构建位置Java Provider 类中XML 标签中
条件判断Java 的 if / switchXML 的 <if>、<choose>
可读性Java 代码更利于 IDE 调试和重构XML 结构对前端开发者更直观
维护成本SQL 分散在 Java 类中SQL 集中在 XML 文件中
功能完整性完全等价完全等价

适用场景建议:团队以注解为主、且动态条件逻辑复杂时优先使用 @SelectProvider;SQL 需要 DBA 集中审核、或动态条件极多时建议使用 XML。

适用位置与核心属性

@SelectProvider 标注在 Mapper 接口的方法 上。

属性类型必填说明
typeClass<?>是Provider 类的 Class 对象,该类包含生成 SQL 的方法
methodString否Provider 类中生成 SQL 的方法名。MyBatis 3.5.1+ 若 Provider 实现 ProviderMethodResolver 接口,可省略此属性

核心原理

MyBatis 在构建 MappedStatement 时,发现方法上标注了 @SelectProvider,会记录 type 和 method。执行查询时,MyBatis 通过反射调用 Provider 类的指定方法,传入当前方法参数,获取返回的 SQL 字符串。该字符串随后进入正常的 SQL 解析流程:参数绑定、预编译、执行。

完整示例

场景说明

乐途公司学生管理系统需要实现一个动态查询功能:根据学生名称、专业、最低分数等多个条件组合查询。用户可能只输入部分条件,需要动态构建 WHERE 子句。

操作前的数据库表结构及初始数据

CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT,
    major VARCHAR(20),
    score DECIMAL(5,2)
);

INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);

当前数据状态:

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00

完整的注解接口代码

查询条件封装类

package com.flying.entity;

public class StudentQuery {
    private String name;
    private String major;
    private Double minScore;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getMajor() { return major; }
    public void setMajor(String major) { this.major = major; }
    public Double getMinScore() { return minScore; }
    public void setMinScore(Double minScore) { this.minScore = minScore; }
}

实体类

package com.flying.entity;

public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String major;
    private Double score;

    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public String getMajor() { return major; }
    public void setMajor(String major) { this.major = major; }
    public Double getScore() { return score; }
    public void setScore(Double score) { this.score = score; }

    @Override
    public String toString() {
        return "Student{id=" + id + ", name='" + name + "', age=" + age +
               ", major='" + major + "', score=" + score + "}";
    }
}

Provider 类

package com.flying.provider;

import com.flying.entity.StudentQuery;
import org.apache.ibatis.jdbc.SQL;

public class StudentSqlProvider {

    /**
     * 根据动态条件构建查询 SQL
     */
    public String selectByCondition(final StudentQuery query) {
        return new SQL() {{
            SELECT("id, name, age, major, score");
            FROM("student");
            if (query.getName() != null && !query.getName().isEmpty()) {
                WHERE("name LIKE CONCAT('%', #{name}, '%')");
            }
            if (query.getMajor() != null && !query.getMajor().isEmpty()) {
                WHERE("major = #{major}");
            }
            if (query.getMinScore() != null) {
                WHERE("score >= #{minScore}");
            }
        }}.toString();
    }

    /**
     * MyBatis 3.5.1+ 可实现 ProviderMethodResolver 接口
     * 让方法名与 Mapper 方法名一致,从而省略 @SelectProvider 的 method 属性
     */
    public String selectByConditionProviderMethodResolver() {
        // 配合 ProviderMethodResolver 使用时的示例方法
        return "SELECT id, name, age, major, score FROM student";
    }
}

Mapper 接口

package com.flying.mapper;

import com.flying.entity.Student;
import com.flying.entity.StudentQuery;
import com.flying.provider.StudentSqlProvider;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.SelectProvider;
import java.util.List;

public interface StudentMapper {

    /**
     * 动态条件查询
     * type 指定 Provider 类,method 指定生成 SQL 的方法名
     */
    @SelectProvider(type = StudentSqlProvider.class, method = "selectByCondition")
    List<Student> selectByCondition(@Param("name") String name,
                                  @Param("major") String major,
                                  @Param("minScore") Double minScore);

    /**
     * 使用对象封装参数的动态查询
     */
    @SelectProvider(type = StudentSqlProvider.class, method = "selectByCondition")
    List<Student> selectByConditionObject(StudentQuery query);
}

测试调用代码

package com.flying.test;

import com.flying.entity.Student;
import com.flying.entity.StudentQuery;
import com.flying.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;
import java.util.List;

public class SelectProviderTest {
    public static void main(String[] args) throws Exception {
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
        SqlSession session = factory.openSession();

        StudentMapper mapper = session.getMapper(StudentMapper.class);

        // 1. 只按名称模糊查询
        System.out.println("=== 只按名称查询 ===");
        List<Student> list1 = mapper.selectByCondition("小", null, null);
        list1.forEach(System.out::println);

        // 2. 按专业和最低分数查询
        System.out.println("\n=== 按专业和最低分数查询 ===");
        List<Student> list2 = mapper.selectByCondition(null, "计算机科学", 90.0);
        list2.forEach(System.out::println);

        // 3. 使用对象参数,三个条件都传
        System.out.println("\n=== 对象参数,多条件组合 ===");
        StudentQuery query = new StudentQuery();
        query.setName("大");
        query.setMajor("计算机科学");
        query.setMinScore(90.0);
        List<Student> list3 = mapper.selectByConditionObject(query);
        list3.forEach(System.out::println);

        // 4. 无条件查询
        System.out.println("\n=== 无条件查询 ===");
        List<Student> list4 = mapper.selectByCondition(null, null, null);
        list4.forEach(System.out::println);

        session.close();
    }
}

实际执行结果

控制台 SQL 输出

=== 只按名称查询 ===
[main] DEBUG com.flying.mapper.StudentMapper.selectByCondition - ==>  Preparing: SELECT id, name, age, major, score FROM student WHERE name LIKE CONCAT('%', ?, '%')
[main] DEBUG com.flying.mapper.StudentMapper.selectByCondition - ==> Parameters: 小(String)
[main] DEBUG com.flying.mapper.StudentMapper.selectByCondition - <==      Total: 2
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.0}
Student{id=5, name='李眉', age=22, major='软件工程', score=87.0}

=== 按专业和最低分数查询 ===
[main] DEBUG com.flying.mapper.StudentMapper.selectByCondition - ==>  Preparing: SELECT id, name, age, major, score FROM student WHERE major = ? AND score >= ?
[main] DEBUG com.flying.mapper.StudentMapper.selectByCondition - ==> Parameters: 计算机科学(String), 90.0(Double)
[main] DEBUG com.flying.mapper.StudentMapper.selectByCondition - <==      Total: 2
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.5}
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.0}

=== 对象参数,多条件组合 ===
[main] DEBUG com.flying.mapper.StudentMapper.selectByConditionObject - ==>  Preparing: SELECT id, name, age, major, score FROM student WHERE name LIKE CONCAT('%', ?, '%') AND major = ? AND score >= ?
[main] DEBUG com.flying.mapper.StudentMapper.selectByConditionObject - ==> Parameters: 大(String), 计算机科学(String), 90.0(Double)
[main] DEBUG com.flying.mapper.StudentMapper.selectByConditionObject - <==      Total: 1
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.5}

=== 无条件查询 ===
[main] DEBUG com.flying.mapper.StudentMapper.selectByCondition - ==>  Preparing: SELECT id, name, age, major, score FROM student
[main] DEBUG com.flying.mapper.StudentMapper.selectByCondition - ==> Parameters:
[main] DEBUG com.flying.mapper.StudentMapper.selectByCondition - <==      Total: 5
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.5}
Student{id=2, name='白歌', age=21, major='软件工程', score=88.0}
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.0}
Student{id=4, name='黄俪', age=21, major='信息安全', score=90.5}
Student{id=5, name='李眉', age=22, major='软件工程', score=87.0}

查询结果集表格

查询条件生成的 SQL结果
name="小"WHERE name LIKE CONCAT('%', ?, '%')小崔、李眉
major="计算机科学", minScore=90.0WHERE major = ? AND score >= ?大翔、小崔
name="大", major="计算机科学", minScore=90.0WHERE name LIKE ... AND major = ? AND score >= ?大翔
无无 WHERE全部 5 人

分析

  • org.apache.ibatis.jdbc.SQL 是 MyBatis 提供的 SQL 构建工具类,支持链式调用 SELECT、FROM、WHERE,并自动处理 AND 前缀
  • Provider 方法返回的字符串就是最终执行的 SQL,MyBatis 会将其中的 #{} 进行参数绑定
  • 多参数场景下,Provider 方法参数与 Mapper 方法参数一致,MyBatis 通过 @Param 将参数传入 Provider
  • MyBatis 3.5.1+ 支持 ProviderMethodResolver 接口,Provider 类实现该接口后,@SelectProvider 可以省略 method 属性,MyBatis 自动根据 Mapper 方法名查找 Provider 中同名方法

易错场景 / 常见误区

误区错误示例正解
Provider 方法不是 publicString selectByCondition(...) 为 privateProvider 方法必须是 public
method 名称写错或大小写不匹配method = "selectbycondition"必须与 Provider 类中的方法名完全一致
SQL 中 #{} 名称与 @Param 不一致@Param("stuName") 但 SQL 写 #{name}必须保持一致
认为 Provider 只能用于 @Select—还有 @InsertProvider、@UpdateProvider、@DeleteProvider
动态 SQL 拼接时产生语法错误WHERE 前多一个 AND使用 org.apache.ibatis.jdbc.SQL 类,它会自动处理 WHERE 和 AND 的拼接

面试考点

Q1:@SelectProvider 中的 type 和 method 分别代表什么?

type 指定 Provider 类的 Class 对象,该类负责动态构建 SQL;method 指定 Provider 类中具体生成 SQL 的方法名。MyBatis 通过反射调用该方法获取 SQL 字符串。

Q2:MyBatis 3.5.1+ 如何省略 @SelectProvider 的 method 属性?

Provider 类实现 ProviderMethodResolver 接口,该接口要求实现 getMethod 方法或遵循默认规则。默认规则下,MyBatis 会自动根据 Mapper 接口的方法名,在 Provider 类中查找同名方法,从而省略 method 属性的显式声明。

Q3:@SelectProvider 和 XML 动态 SQL 各有什么优劣?

@SelectProvider 将 SQL 构建逻辑放在 Java 代码中,便于 IDE 调试、重构和单元测试,适合以 Java 为主的团队;XML 动态 SQL 结构清晰,SQL 与 Java 分离,便于 DBA 审核和集中管理,适合 SQL 复杂或需要频繁调整的项目。

Q4:Provider 类中可以使用第三方 SQL 构建工具吗?

可以。Provider 方法只需要返回一个 SQL 字符串即可,内部可以使用 org.apache.ibatis.jdbc.SQL、StringBuilder、第三方库(如 JOOQ、QueryDSL)等任何方式构建 SQL。

小结

@SelectProvider 是注解开发中实现动态 SQL 的核心方案。通过 Provider 类将 SQL 构建逻辑外置,既保留了注解的简洁性,又获得了动态条件的灵活性。配合 org.apache.ibatis.jdbc.SQL 工具类,可以安全高效地拼接 WHERE 子句。对于插入、更新、删除的动态场景,可分别使用 @InsertProvider、@UpdateProvider、@DeleteProvider,用法完全一致。

下一章引子

至此,MyBatis 注解开发的核心注解已全部讲解完毕。从基础的 @Select、@Insert、@Update、@Delete,到结果映射的 @Results、@Result,再到关联映射的 @One、@Many,以及参数命名的 @Param、语句选项的 @Options、主键获取的 @SelectKey、动态 SQL 的 @SelectProvider,你已经具备了纯注解开发 MyBatis 的完整能力。建议在实际项目中根据 SQL 复杂度灵活选择注解或 XML,让持久层代码既简洁又可维护。

上一页
@Many