sql片段
导学
本节将掌握MyBatis中sql片段元素的定义与复用机制。你将学会如何提取重复的SQL代码为可复用片段,理解include标签的引用方式,掌握通过property传递参数实现动态片段,并明确sql片段在大型项目中的最佳实践。
定义
sql是SQL映射文件中用于定义可复用SQL代码段的元素。在JDBC原始写法中,若多个查询需要相同的字段列表或相同的WHERE条件,开发者只能在每个SQL字符串中重复书写,一旦字段变更(如新增一列),所有相关SQL都需要逐一修改,极易遗漏。sql元素将公共SQL片段集中定义,通过include标签在多处引用,实现"一处定义,处处复用",大幅提升映射文件的可维护性。
适用位置与核心属性
sql元素书写在映射文件的<mapper>根标签内部,可被同一文件内的任何select、insert、update、delete引用。
| 属性 | 是否必填 | 说明 |
|---|---|---|
id | 是 | sql片段的唯一标识,供include标签引用 |
include标签用于引用sql片段:
| 属性 | 是否必填 | 说明 |
|---|---|---|
refid | 是 | 引用的sql片段ID |
include内部可通过property子元素向sql片段传递参数:
| 子元素 | 说明 |
|---|---|
<property name="xxx" value="yyy"/> | 向sql片段传递名值对,在片段内通过${xxx}接收 |
核心原理
MyBatis解析映射文件时,会将include标签替换为对应sql片段的内容,形成完整的SQL语句后再进入参数绑定和执行阶段。
- 片段注册:MyBatis启动时扫描所有
<sql>元素,按id注册到XMLMapperBuilder的sqlFragments集合中。 - 引用解析:处理
select/update/delete/insert时,遇到<include>标签,根据refid查找对应的片段内容。 - 内容替换:将
<include>及其子元素替换为<sql>片段的原始内容。 - 参数传递:若
<include>中包含<property>,将value替换片段中对应的${name}占位符。 - SQL生成:替换完成后,继续解析动态SQL(如
if、foreach等),最终生成可执行的SQL。
完整示例
场景说明
乐途公司技术部的学员管理系统中有多个查询都需要相同的字段列表(id, name, age, major, score),以及一个常用的按专业筛选条件。本节演示将字段列表和公共条件提取为sql片段,在多个select中复用,并通过property实现动态表别名前缀。
操作前的数据库表结构及初始数据
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);
当前数据状态:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
完整的映射文件片段与Java代码
POJO类
package com.flywing.entity;
public class Student {
private Integer id;
private String name;
private Integer age;
private String major;
private Double score;
// Getter与Setter省略
}
Mapper接口
package com.flywing.mapper;
import com.flywing.entity.Student;
import java.util.List;
import java.util.Map;
public interface StudentMapper {
// 查询所有学员(复用字段列表片段)
List<Student> findAll();
// 按专业查询(复用字段列表+条件片段)
List<Student> findByMajor(String major);
// 多表关联查询(复用带别名的字段列表片段)
List<Student> findAllWithAlias();
}
映射文件 StudentMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.flywing.mapper.StudentMapper">
<!--
sql片段一:公共字段列表
所有查询学生信息的语句都可以引用此片段,避免字段名散落在各处
-->
<sql id="studentColumns">
id, name, age, major, score
</sql>
<!--
sql片段二:带动态表别名的字段列表
通过property传递前缀,适用于多表关联场景
-->
<sql id="studentColumnsWithPrefix">
${prefix}.id, ${prefix}.name, ${prefix}.age, ${prefix}.major, ${prefix}.score
</sql>
<!--
sql片段三:公共查询条件
按专业筛选是系统中高频使用的条件
-->
<sql id="majorCondition">
AND major = #{major}
</sql>
<!-- 场景一:查询所有,引用字段列表片段 -->
<select id="findAll" resultType="com.flywing.entity.Student">
SELECT
<include refid="studentColumns"/>
FROM student
</select>
<!-- 场景二:按专业查询,引用字段列表片段和条件片段 -->
<select id="findByMajor" resultType="com.flywing.entity.Student">
SELECT
<include refid="studentColumns"/>
FROM student
WHERE 1 = 1
<include refid="majorCondition"/>
</select>
<!--
场景三:多表关联查询,引用带前缀的字段列表片段
通过property传递表别名s
-->
<select id="findAllWithAlias" resultType="com.flywing.entity.Student">
SELECT
<include refid="studentColumnsWithPrefix">
<property name="prefix" value="s"/>
</include>
FROM student s
</select>
</mapper>
测试代码
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.List;
public class SqlFragmentTest {
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);
// 场景一:查询所有
System.out.println("=== 查询所有学员 ===");
List<Student> all = mapper.findAll();
System.out.println("总人数:" + all.size());
for (Student s : all) {
System.out.println(" " + s.getId() + " | " + s.getName() + " | " + s.getMajor());
}
// 场景二:按专业查询
System.out.println("\n=== 按专业查询(计算机科学)===");
List<Student> csList = mapper.findByMajor("计算机科学");
System.out.println("计算机科学专业人数:" + csList.size());
for (Student s : csList) {
System.out.println(" " + s.getName() + " | " + s.getScore());
}
// 场景三:带别名查询
System.out.println("\n=== 带别名查询 ===");
List<Student> aliasList = mapper.findAllWithAlias();
System.out.println("带别名查询人数:" + aliasList.size());
for (Student s : aliasList) {
System.out.println(" " + s.getName() + " | " + s.getAge());
}
session.close();
}
}
实际执行结果
控制台SQL输出
=== 查询所有学员 ===
[DEBUG] com.flywing.mapper.StudentMapper.findAll - ==> Preparing: SELECT id, name, age, major, score FROM student
[DEBUG] com.flywing.mapper.StudentMapper.findAll - <== Total: 5
总人数:5
1 | 大翔 | 计算机科学
2 | 白歌 | 软件工程
3 | 小崔 | 计算机科学
4 | 黄俪 | 信息安全
5 | 李眉 | 软件工程
=== 按专业查询(计算机科学)===
[DEBUG] com.flywing.mapper.StudentMapper.findByMajor - ==> Preparing: SELECT id, name, age, major, score FROM student WHERE 1 = 1 AND major = ?
[DEBUG] com.flywing.mapper.StudentMapper.findByMajor - ==> Parameters: 计算机科学(String)
[DEBUG] com.flywing.mapper.StudentMapper.findByMajor - <== Total: 2
计算机科学专业人数:2
大翔 | 95.5
小崔 | 92.0
=== 带别名查询 ===
[DEBUG] com.flywing.mapper.StudentMapper.findAllWithAlias - ==> Preparing: SELECT s.id, s.name, s.age, s.major, s.score FROM student s
[DEBUG] com.flywing.mapper.StudentMapper.findAllWithAlias - <== Total: 5
带别名查询人数:5
大翔 | 22
白歌 | 21
小崔 | 20
黄俪 | 21
李眉 | 22
查询结果集表格(以findByMajor为例)
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
分析
- 字段列表复用:
studentColumns片段将5个字段集中管理。若未来需要新增字段(如email),只需修改片段一处,所有引用该片段的查询自动生效,彻底消除遗漏风险。 - 条件片段复用:
majorCondition片段封装了按专业筛选的逻辑。虽然本例较为简单,但在复杂系统中,公共条件可能包含多表关联、权限过滤等逻辑,提取为片段后可在数十个查询中统一维护。 - 动态前缀传递:
studentColumnsWithPrefix通过${prefix}接收include传递的property参数。这在多表关联查询中极为实用:同一张表在不同查询中可能使用不同别名,通过参数化前缀避免为每个别名定义重复的片段。
易错场景/常见误区
| 误区 | 正解 |
|---|---|
在sql片段中写#{}参数 | sql片段中可以使用#{},但片段本身不绑定参数;参数由引用它的select/insert等语句提供 |
在sql片段的property中用#{}接收 | property传递的值在片段内通过${}接收(字符串替换),不是#{}(预编译绑定) |
refid写错片段ID | refid必须与<sql>的id完全匹配,包括大小写;建议统一使用小写加下划线命名 |
认为sql片段可以跨文件引用 | 默认只能引用同一映射文件内的片段;跨文件引用需使用命名空间前缀,如<include refid="com.flywing.mapper.OtherMapper.commonColumns"/> |
将整条SQL(含WHERE子句)放入sql片段 | sql片段应只包含可复用的最小单元(字段列表、条件片段),整条SQL的复用应通过Mapper接口设计实现 |
面试考点
Q1:sql片段和include的作用是什么?
sql片段用于将映射文件中重复的SQL代码(如字段列表、公共条件)提取为可复用模块,include用于在select、insert、update、delete中引用这些模块。核心价值是减少重复代码、统一维护入口,当字段或条件变更时只需修改片段定义处。
Q2:include的property子元素是如何工作的?
property通过名值对向sql片段传递参数,片段内部通过${name}(字符串替换)接收。注意这里使用${}而非#{},因为片段替换发生在SQL生成阶段,不是参数绑定阶段。传递的值通常是表别名、列前缀等元数据,不应来自用户输入。
Q3:sql片段能否跨Mapper文件引用?
可以,但需要在
refid中指定命名空间前缀,格式为namespace.sqlId。例如<include refid="com.flywing.mapper.CommonMapper.baseColumns"/>。被引用的片段所在的Mapper文件必须已被MyBatis加载。
Q4:sql片段与动态SQL(如if、foreach)能否结合使用?
可以。
sql片段内部可以包含动态SQL标签。但需要注意:片段被解析替换后,动态SQL才会执行。因此片段中的动态条件可以正常工作,但片段本身不能独立执行,必须嵌入到select等语句中。
小结
sql片段是MyBatis映射文件中的"代码复用器"。通过将公共字段列表、条件逻辑提取为片段,开发者可以避免重复书写、降低维护成本。include标签实现片段引用,property子元素支持动态参数传递,使片段在多表关联等复杂场景下依然灵活可用。合理使用sql片段,是让大型项目的映射文件保持整洁的关键手段。
下一章引子
至此,第03章"SQL映射文件基础"的全部内容已讲解完毕。你已经掌握了select、insert、update、delete四大基本映射元素,resultType与resultMap的结果映射机制,#{}与${}的参数传递原理,主键生成策略,以及sql片段的复用技巧。但现实中的SQL远比基础写法复杂:查询条件可能动态变化,更新字段可能部分为空,批量操作需要遍历集合。第04章"动态SQL"将引入if、choose、where、set、foreach、bind和trim七大标签,彻底解决动态SQL拼接的难题。