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

    • 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执行器类型
    • 分页插件

SQL 语句构建器

导学

本节学习目标:

  • 理解 MyBatis SQL 类的设计初衷:在 Java 代码中动态、类型安全地构建 SQL 语句
  • 掌握 SQL 类的完整 API(查询、关联、条件、增删改、分页、批量插入)
  • 能够熟练使用匿名内部类双括号初始化语法编写 Provider 方法
  • 掌握 @SelectProvider、@InsertProvider 等注解与 Provider 类的配合方式
  • 能够根据业务场景判断:何时使用 Java SQL 构建器,何时使用 XML 动态 SQL

定义

在 MyBatis 中,大多数 SQL 通过 XML 映射文件或注解静态声明。但当 WHERE 条件、SET 列、VALUES 行数需要在运行时根据传入参数动态决定时,静态声明显得力不从心。MyBatis 3 提供了 org.apache.ibatis.jdbc.SQL 类,允许开发者以链式方法调用的方式在 Java 代码中拼接 SQL,既保留了动态灵活性,又避免了手写字符串拼接带来的 SQL 注入风险与维护噩梦。

SQL 类通常与 @SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 注解配合使用:注解指定 Provider 类及方法名,Provider 方法内部通过 SQL 类构建并返回最终 SQL 字符串,MyBatis 将其作为映射语句执行。


适用位置与核心 API

SQL 类位于 org.apache.ibatis.jdbc 包下,通过方法链构建 SQL。所有方法均返回 SQL 实例自身,支持连续调用。

方法类别方法名说明版本
查询SELECT(String... columns)指定查询列3.0+
查询SELECT_DISTINCT(String... columns)去重查询3.0+
表FROM(String... tables)指定表名3.0+
关联JOIN(String... tables) / INNER_JOIN(...)内连接3.0+
关联LEFT_OUTER_JOIN(...) / RIGHT_OUTER_JOIN(...)左/右外连接3.0+
条件WHERE(String... conditions)WHERE 子句3.0+
条件AND(String... conditions) / OR(...)追加 AND / OR3.0+
分组GROUP_BY(String... columns) / HAVING(...)分组与过滤3.0+
排序ORDER_BY(String... columns)排序3.0+
分页LIMIT(String variable) / OFFSET(...)限制行数 / 偏移量3.5.2+
插入INSERT_INTO(String table) / VALUES(String... columns)单条插入3.0+
批量插入ADD_ROW()追加一行(配合 VALUES 使用)3.5.2+
更新UPDATE(String table) / SET(String... sets)更新3.0+
删除DELETE_FROM(String table)删除3.0+
其他toString()生成最终 SQL 字符串3.0+

版本提示:LIMIT / OFFSET / ADD_ROW 需要 MyBatis 3.5.2+,配合 MySQL 5.7 分页与批量插入非常实用。


核心原理

SQL 构建器工作流程

  1. Mapper 接口方法标注 @SelectProvider(type = XxxProvider.class, method = "xxx")
  2. MyBatis 在运行时反射调用 Provider 类的指定方法,传入与 Mapper 接口相同的参数对象
  3. Provider 方法内部通过 new SQL() {{ ... }} 双括号初始化语法链式拼接 SQL
  4. SQL 类将方法调用转换为标准 SQL 片段,自动处理空格与关键字衔接
  5. 返回的 SQL 字符串被 MyBatis 当作映射语句解析、预编译并执行
  6. 结果通过 ResultHandler 映射后返回

完整示例

沿用 student 表数据:

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

场景一:动态条件查询(根据名称和分数范围动态构建 WHERE)

场景说明

乐途公司教务系统需要提供一个学员综合查询页面:用户可输入姓名(模糊匹配)和最低分数,两个条件均为选填。若参数为空,则不加入该条件。使用 SQL 类在 Java 中动态拼接 WHERE 子句,避免 XML 中冗长的 <if> 标签。

操作前数据状态

见上表,共 5 条记录。

完整 Java 代码

Provider 类

package com.flywing.provider;

import org.apache.ibatis.jdbc.SQL;
import java.util.Map;

public class StudentProvider {

    public String findByCondition(final Map<String, Object> params) {
        return new SQL() {{
            SELECT("id, name, age, major, score");
            FROM("student");
            if (params.get("name") != null && !((String) params.get("name")).isEmpty()) {
                WHERE("name LIKE CONCAT('%', #{name}, '%')");
            }
            if (params.get("minScore") != null) {
                WHERE("score >= #{minScore}");
            }
            ORDER_BY("score DESC");
        }}.toString();
    }
}

Mapper 接口

package com.flywing.mapper;

import com.flywing.entity.Student;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.SelectProvider;
import java.util.List;
import java.util.Map;

public interface StudentMapper {

    @SelectProvider(type = StudentProvider.class, method = "findByCondition")
    List<Student> findByCondition(Map<String, Object> params);
}

测试代码

package com.flywing.test;

import com.flywing.entity.Student;
import com.flywing.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.HashMap;
import java.util.List;
import java.util.Map;

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

        try (SqlSession session = factory.openSession()) {
            StudentMapper mapper = session.getMapper(StudentMapper.class);

            // 场景 1a:两个条件都传
            Map<String, Object> params1 = new HashMap<>();
            params1.put("name", "小");
            params1.put("minScore", 90.0);
            List<Student> list1 = mapper.findByCondition(params1);
            System.out.println("条件:name 包含'小'且 score>=90,结果 " + list1.size() + " 条");
            list1.forEach(s -> System.out.println("  - " + s.getName() + ",分数:" + s.getScore()));

            // 场景 1b:只传 minScore
            Map<String, Object> params2 = new HashMap<>();
            params2.put("minScore", 90.0);
            List<Student> list2 = mapper.findByCondition(params2);
            System.out.println("条件:score>=90,结果 " + list2.size() + " 条");

            // 场景 1c:无参数
            List<Student> list3 = mapper.findByCondition(new HashMap<>());
            System.out.println("条件:无,结果 " + list3.size() + " 条");
        }
    }
}

生成的 SQL 语句

场景生成的 SQL
1a(name + minScore)SELECT id, name, age, major, score FROM student WHERE (name LIKE CONCAT('%', ?, '%') AND score >= ?) ORDER BY score DESC
1b(仅 minScore)SELECT id, name, age, major, score FROM student WHERE (score >= ?) ORDER BY score DESC
1c(无参数)SELECT id, name, age, major, score FROM student ORDER BY score DESC

执行结果表格

场景 1a 结果:

idnameagemajorscore
3小崔20计算机科学92.00

场景 1b 结果:

idnameagemajorscore
1大翔22计算机科学95.50
3小崔20计算机科学92.00
4黄俪21信息安全90.50

场景 1c 结果:

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

分析

  • SQL 类的 WHERE() 方法在内部会自动处理多个条件之间的 AND 连接,无需手动写 AND 关键字
  • 当 params 中某键为 null 时,Provider 方法跳过该条件,实现真正的动态 SQL
  • #{name} 与 #{minScore} 仍由 MyBatis 预编译绑定,防止 SQL 注入
  • 与 XML 动态 SQL 相比,Java 构建器更适合条件逻辑复杂、需要调用外部工具类或常量枚举的场景

场景二:动态更新(根据非空字段动态构建 SET)

场景说明

乐途公司学员信息编辑页面允许修改姓名、年龄、专业、分数中的任意字段,未填写的字段保持原值不变。使用 SQL 类动态构建 UPDATE ... SET 语句,避免将未修改字段覆盖为 null。

操作前数据状态

更新前 id=1(大翔)的记录:

idnameagemajorscore
1大翔22计算机科学95.50

完整 Java 代码

Provider 类(追加方法)

package com.flywing.provider;

import com.flywing.entity.Student;
import org.apache.ibatis.jdbc.SQL;

public class StudentProvider {

    // ... 场景一的方法保留 ...

    public String updateDynamic(final Student student) {
        return new SQL() {{
            UPDATE("student");
            if (student.getName() != null && !student.getName().isEmpty()) {
                SET("name = #{name}");
            }
            if (student.getAge() != null) {
                SET("age = #{age}");
            }
            if (student.getMajor() != null && !student.getMajor().isEmpty()) {
                SET("major = #{major}");
            }
            if (student.getScore() != null) {
                SET("score = #{score}");
            }
            WHERE("id = #{id}");
        }}.toString();
    }
}

Mapper 接口(追加方法)

package com.flywing.mapper;

import com.flywing.entity.Student;
import org.apache.ibatis.annotations.UpdateProvider;

public interface StudentMapper {

    // ... 场景一的方法保留 ...

    @UpdateProvider(type = StudentProvider.class, method = "updateDynamic")
    int updateDynamic(Student student);
}

测试代码(追加)

// 在 SqlBuilderDemo.main 中追加:

// 场景二:只修改 name 和 score,age 和 major 保持原值
Student toUpdate = new Student();
toUpdate.setId(1);
toUpdate.setName("大翔-已修改");
toUpdate.setScore(98.5);
// age 和 major 为 null,Provider 中不会生成对应 SET 项

int rows = mapper.updateDynamic(toUpdate);
session.commit();
System.out.println("更新影响行数:" + rows);

// 查询验证
Student updated = mapper.findById(1);
System.out.println("更新后:name=" + updated.getName()
    + ", age=" + updated.getAge()
    + ", major=" + updated.getMajor()
    + ", score=" + updated.getScore());

生成的 SQL 语句

UPDATE student SET name = ?, score = ? WHERE (id = ?)

实际参数绑定:大翔-已修改(String), 98.5(BigDecimal), 1(Integer)

执行结果表格

更新后 id=1 的记录:

idnameagemajorscore
1大翔-已修改22计算机科学98.50

分析

  • SET() 方法在内部会自动将多个 SET 项用逗号连接,无需手动处理末尾逗号问题
  • 若传入对象所有可选字段均为 null,则生成 UPDATE student WHERE (id = ?),这在 MySQL 5.7 中是语法错误。生产代码中应在 Provider 或 Service 层做兜底校验,确保至少有一个 SET 项
  • 与 XML 的 <set> + <if> 相比,Java 构建器可以直接调用 student.getXxx(),无需通过 OGNL 表达式访问属性,类型安全且 IDE 支持更好

场景三:批量插入(使用 ADD_ROW)

场景说明

乐途公司每年秋季批量录入新生信息。使用 MyBatis 3.5.2+ 提供的 ADD_ROW() 方法,在 Java 中构建多行 INSERT 语句,一次性将多条记录写入 MySQL 5.7,减少网络往返。

操作前数据状态

当前 student 表已有 5 条记录(id 1~5)。

完整 Java 代码

Provider 类(追加方法)

package com.flywing.provider;

import com.flywing.entity.Student;
import org.apache.ibatis.jdbc.SQL;
import java.util.List;

public class StudentProvider {

    // ... 前两个场景的方法保留 ...

    public String batchInsert(final List<Student> students) {
        return new SQL() {{
            INSERT_INTO("student");
            VALUES("name, age, major, score", "#{list[0].name}, #{list[0].age}, #{list[0].major}, #{list[0].score}");
            for (int i = 1; i < students.size(); i++) {
                ADD_ROW();
                VALUES("name, age, major, score",
                    "#{list[" + i + "].name}, #{list[" + i + "].age}, #{list[" + i + "].major}, #{list[" + i + "].score}");
            }
        }}.toString();
    }
}

注意:ADD_ROW() 从 3.5.2 开始支持,用于生成 VALUES (...), (...), (...) 的多行语法。

Mapper 接口(追加方法)

package com.flywing.mapper;

import com.flywing.entity.Student;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Param;
import java.util.List;

public interface StudentMapper {

    // ... 前两个场景的方法保留 ...

    @InsertProvider(type = StudentProvider.class, method = "batchInsert")
    int batchInsert(@Param("list") List<Student> students);
}

必须使用 @Param("list"),Provider 中通过 #{list[i].xxx} 访问列表元素。

测试代码(追加)

// 在 SqlBuilderDemo.main 中追加:

// 场景三:批量插入 2 名新生
Student s6 = new Student();
s6.setName("周舟");
s6.setAge(19);
s6.setMajor("人工智能");
s6.setScore(91.0);

Student s7 = new Student();
s7.setName("吴双");
s7.setAge(20);
s7.setMajor("数据科学");
s7.setScore(89.5);

List<Student> newStudents = Arrays.asList(s6, s7);
int inserted = mapper.batchInsert(newStudents);
session.commit();
System.out.println("批量插入影响行数:" + inserted);

// 查询验证
List<Student> all = mapper.findByCondition(new HashMap<>());
System.out.println("当前总记录数:" + all.size());
all.stream().filter(s -> s.getId() > 5).forEach(s ->
    System.out.println("  新生:" + s.getName() + ",id=" + s.getId()));

生成的 SQL 语句

INSERT INTO student (name, age, major, score)
VALUES (?, ?, ?, ?), (?, ?, ?, ?)

实际参数绑定:

  • 第 1 行:周舟, 19, 人工智能, 91.0
  • 第 2 行:吴双, 20, 数据科学, 89.5

执行结果表格

插入后 student 表新增记录:

idnameagemajorscore
6周舟19人工智能91.00
7吴双20数据科学89.50

当前总记录数:7 条。

分析

  • ADD_ROW() 使 SQL 类具备了生成多行 VALUES 的能力,语法与 MySQL 5.7 的批量插入完全兼容
  • 批量插入相比循环单条插入,网络往返次数从 N 次降为 1 次,性能提升显著(实测可达数倍至数十倍)
  • 若批量数据量极大(如数千条),建议拆分为每批 500~1000 条执行,避免 SQL 过长导致 MySQL max_allowed_packet 超限
  • Provider 中通过 #{list[i].xxx} 绑定参数时,索引必须与 Java 列表顺序一致;若使用 @Param 指定其他名称,Provider 中也要同步修改

易错场景/常见误区

误区错误表现正解
忘记在 Mapper 接口参数上加 @Param("list")批量插入时抛出 BindingException: Parameter 'list' not found当传入集合或数组时,Provider 中通过 #{list[i]} 或 #{array[i]} 访问,必须在接口参数上标注 @Param 以固定名称
在 WHERE() 中手动拼接 AND 或 OR生成 SQL 出现 WHERE (AND name = ?) 语法错误SQL 类的 WHERE() 内部已自动处理连接符;多个条件直接多次调用 WHERE() 即可,它会自动用 AND 拼接
所有字段为 null 时动态更新生成空 SET抛出 MySQLSyntaxErrorException: You have an error in your SQL syntax在 Provider 方法中增加判断:若没有任何字段需要更新,直接返回空字符串或抛出业务异常;或在 Service 层拦截
使用 3.5.2 以下版本调用 ADD_ROW()编译通过但运行时报 NoSuchMethodError确认项目依赖的 MyBatis 版本 ≥ 3.5.2;可通过 mvn dependency:tree 检查实际解析版本
将 SQL 类与 XML 动态 SQL 混用导致维护混乱同一项目中有的 Mapper 用 XML <if>,有的用 Java Provider,风格不统一团队应制定规范:简单动态条件用 XML,复杂逻辑(需调用工具类、枚举、外部配置)用 Java Provider;同一命名空间尽量统一风格
在 Provider 中直接拼接用户输入的字符串SQL 注入风险始终使用 #{xxx} 占位符让 MyBatis 预编译绑定;Provider 中只拼接 SQL 关键字和结构,不拼接用户数据

面试考点

Q1:SQL 类的双括号初始化 new SQL() {{ ... }} 是什么语法?有什么注意事项?

A:这是 Java 的匿名内部类 + 实例初始化块语法。外层 {} 定义了一个继承自 SQL 的匿名类,内层 {{ ... }} 是实例初始化块,在构造器之后执行。注意事项:① 它会产生额外的类文件(Xxx$1.class),大量使用可能增加元空间负担;② 在初始化块中不能抛出受检异常;③ Java 8+ 可改用 Lambda 或方法引用进一步优化,但双括号语法在 Provider 中仍最为直观。

Q2:@SelectProvider 与 XML 动态 SQL(如 <if>)相比,各适用于什么场景?

A:XML 动态 SQL 适用于条件逻辑简单、以 SQL 片段为主的场景,直观且与 DBA 协作方便;Java SQL 构建器适用于:① 条件逻辑复杂,需要调用 Java 工具类、枚举或外部配置;② 需要运行时动态决定表名、列名;③ 与注解式 Mapper(无 XML)配合保持风格统一。两者在运行时最终都生成字符串 SQL,由 MyBatis 解析执行,性能无本质差异。

Q3:Provider 方法的返回值类型为什么必须是 String?能否返回其他类型?

A:@SelectProvider 等注解要求 Provider 方法返回 String 类型,该字符串即为最终执行的 SQL。MyBatis 3.5.1+ 也支持 Provider 方法返回 CharSequence 或其子类。不能返回 SqlNode、MappedStatement 等内部类型,因为注解解析层只做了 String 转换。

Q4:批量插入时,ADD_ROW() 生成的 SQL 长度有限制吗?如何规避?

A:受限于 MySQL 5.7 的 max_allowed_packet 配置(默认 4MB 或 64MB,依版本而异)。单条 SQL 过长会导致 Packet too large 错误。规避方案:① 在 Provider 或 Service 层将大数据集拆分为每批 500~1000 条;② 调整 MySQL 的 max_allowed_packet 参数(治标不治本,不建议过度调大);③ 对于超大规模数据,改用 MyBatis 的 SqlSession 批量模式(ExecutorType.BATCH)或数据库 LOAD DATA 方式。


小结

org.apache.ibatis.jdbc.SQL 是 MyBatis 为 Java 开发者提供的"动态 SQL 瑞士军刀"。通过链式 API,开发者可以在类型安全、IDE 友好的环境中构建复杂查询、动态更新和批量插入,彻底告别字符串拼接的脆弱与丑陋。掌握 SELECT、WHERE、SET、VALUES、ADD_ROW 等核心方法,理解 Provider 注解的协作机制,并根据团队规范在 XML 与 Java 构建器之间做出合理选择,是提升 MyBatis 工程化水平的重要一步。


下一章引子

SQL 语句构建器解决了 Java 代码中动态拼接 SQL 的需求,但大多数项目仍以 XML 映射文件为主战场。当查询条件多变、需要处理 null 参数、循环遍历集合时,XML 中的 <if>、<choose>、<where>、<set>、<foreach> 等动态 SQL 标签才是更经典的解决方案。接下来,我们将进入 MyBatis 动态 SQL 的完整世界,学习如何在 XML 中优雅地处理复杂逻辑。

上一页
sql片段