一、SQL 映射文件核心元素
MyBatis 映射文件的顶级元素(按定义顺序):
cache
:命名空间的缓存配置。cache-ref
:引用其他命名空间的缓存。resultMap
:自定义结果集映射。sql
:可重用的 SQL 片段。insert
、update
、delete
:数据操作语句。select
:查询语句。
二、参数传递与处理
1. 单参数
-基础类型/字符串:
<select id="selectUser" resultType="User" parameterType="int">
SELECT * FROM user WHERE id = #{id}
</select>
-POJO 对象:
<insert id="insertUser" parameterType="User">
INSERT INTO user (name, email) VALUES (#{name}, #{email})
</insert>
2. 多参数
- 默认 param1
, param2
(不推荐):
<select id="selectUser" resultType="User">
SELECT * FROM user WHERE id = #{param1} AND name = #{param2}
</select>
- @Param
注解(推荐):
User selectUser(@Param("id") int id, @Param("name") String name);
<select id="selectUser" resultType="User">
SELECT * FROM user WHERE id = #{id} AND name = #{name}
</select>
3. 复杂参数
- Map 类型:
<select id="selectUserByMap" resultType="User" parameterType="map">
SELECT * FROM user WHERE name = #{name} AND age = #{age}
</select>
- 混合参数(POJO + @Param
):
List<User> selectUsers(@Param("role") String role, User user);
<select id="selectUsers" resultType="User">
SELECT * FROM user WHERE role = #{role} AND age = #{user.age}
</select>
三、主键生成与回填
1. 自增主键(如 MySQL)
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (name, email) VALUES (#{name}, #{email})
</insert>
useGeneratedKeys="true"
:启用 JDBC 的自动生成主键。keyProperty="id"
:将生成的主键赋值给对象的id
属性。
2. 非自增主键(如 Oracle)
<insert id="insertUser">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
SELECT MAX(id) + 1 FROM user
</selectKey>
INSERT INTO user (id, name) VALUES (#{id}, #{name})
</insert>
order="BEFORE"
:先执行selectKey
生成主键,再插入数据。
四、结果映射(resultMap
)
1. 基础映射
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="name" column="user_name" />
</resultMap>
2. 关联对象(一对一)
<resultMap id="userWithRoleMap" type="User">
<association property="role" javaType="Role">
<id property="roleId" column="role_id" />
<result property="roleName" column="role_name" />
</association>
</resultMap>
3. 集合映射(一对多)
<resultMap id="userWithOrdersMap" type="User">
<collection property="orders" ofType="Order">
<id property="orderId" column="order_id" />
<result property="orderNo" column="order_no" />
</collection>
</resultMap>
五、动态 SQL
1. 条件查询(<if>
+ <where>
)
<select id="selectUser" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</where>
</select>
2. 循环遍历(<foreach>
)
<select id="selectUsersByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
1. 参数是 List
当方法参数直接传递一个 List
时,MyBatis 默认将其封装为 Map
,键为 list
。
示例代码:
// DAO 方法
List<User> selectUsersByIds(@Param("ids") List<Integer> ids);
<!-- XML 映射 -->
<select id="selectUsersByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
关键点:
collection="ids"
:对应@Param("ids")
注解的参数名。item="id"
:遍历的每个元素变量名。open="("
和close=")"
:包裹生成的 SQL 片段。separator=","
:元素之间的分隔符。
2. 参数是 Set
Set
的处理方式与 List
类似,MyBatis 会自动将其转换为 List
处理。
示例代码:
// DAO 方法
List<User> selectUsersByNames(@Param("names") Set<String> names);
<!-- XML 映射 -->
<select id="selectUsersByNames" resultType="User">
SELECT * FROM user
WHERE name IN
<foreach collection="names" item="name" open="(" separator="," close=")">
#{name}
</foreach>
</select>
3. 参数是数组
当直接传递数组时,collection
属性需指定为 array
。
示例代码:
// DAO 方法
List<User> selectUsersByIds(int[] ids);
<!-- XML 映射 -->
<select id="selectUsersByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="array" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
4. 参数是 Map
中的集合
如果参数是 Map
,需通过 @Param
指定键名。
示例代码:
// DAO 方法
List<User> selectUsers(@Param("data") Map<String, Object> data);
<!-- XML 映射 -->
<select id="selectUsers" resultType="User">
SELECT * FROM user
WHERE
id IN
<foreach collection="data.ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND name IN
<foreach collection="data.names" item="name" open="(" separator="," close=")">
#{name}
</foreach>
</select>
5. 批量插入示例
使用 <foreach>
实现批量插入:
// DAO 方法
void batchInsertUsers(@Param("users") List<User> users);
<!-- XML 映射 -->
<insert id="batchInsertUsers">
INSERT INTO user (name, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.email})
</foreach>
</insert>
6. 遍历 Map
类型
当集合元素是 Map
时,index
和 item
分别代表键和值。
示例代码:
// DAO 方法
void insertUserRoles(@Param("roles") Map<Integer, String> roles);
<!-- XML 映射 -->
<insert id="insertUserRoles">
INSERT INTO role (user_id, role_name) VALUES
<foreach collection="roles" index="userId" item="roleName" separator=",">
(#{userId}, #{roleName})
</foreach>
</insert>
7.关键注意事项
-
参数类型匹配
- 单参数集合需通过
@Param
显式命名。 - 多参数需用
@Param
避免混淆。
- 单参数集合需通过
-
安全与性能
- 始终使用
#{}
占位符(防止 SQL 注入)。 - 批量操作时,注意数据库的 SQL 语句长度限制。
- 始终使用
-
动态 SQL 灵活性
- 结合
<if>
标签实现条件遍历:<foreach collection="ids" item="id"> <if test="id != null"> #{id} </if> </foreach>
- 结合
8.总结
参数类型 | collection 值 | 示例场景 |
---|---|---|
List | @Param 指定的名称 | IN 查询、批量插入 |
Set | @Param 指定的名称 | 去重后的 IN 查询 |
数组 | array | 原生数组参数的 IN 查询 |
Map | map.key 或 @Param | 复杂参数组合的动态查询 |
通过合理使用 <foreach>
,可以高效处理集合类参数,简化批量操作和动态 SQL 的编写。
3. 分支选择(<choose>
)
<select id="selectUserByChoose" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="id != null">id = #{id}</when>
<otherwise>status = 1</otherwise>
</choose>
</where>
</select>
六、高级特性
1. 分步查询与延迟加载
- 分步查询:
<resultMap id="catMap" type="Cat"> <association property="owner" select="selectOwnerById" column="owner_id" /> </resultMap>
- 延迟加载配置:
<settings> <setting name="lazyLoadingEnabled" value="true" /> <setting name="aggressiveLazyLoading" value="false" /> </settings>
2. SQL 片段复用(<sql>
)
<sql id="userColumns">id, name, email</sql>
<select id="selectUser" resultType="User">
SELECT <include refid="userColumns" /> FROM user
</select>
七、特殊处理
1. #{}
与 ${}
以下是 #{}
和 ${}
的核心区别及使用场景总结,结合动态表名/字段的示例说明:
1. #{}
与 ${}
的核心区别
特性 | #{} | ${} |
---|---|---|
原理 | 预编译(PreparedStatement) | 字符串直接拼接(SQL 注入风险) |
安全性 | 防 SQL 注入(推荐) | 不安全(需严格校验输入) |
适用场景 | 参数值(如 WHERE 条件) | 动态表名、列名、排序字段等 |
示例 | WHERE id = #{id} | ORDER BY ${columnName} |
2. 动态表名与字段的示例
场景 1:动态表名(如多租户系统)
<!-- 根据传入的表名查询数据 -->
<select id="selectByDynamicTable" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
- 调用方式:
List<User> users = userDao.selectByDynamicTable("user_2023", 1001);
- 生成 SQL:
SELECT * FROM user_2023 WHERE id = ?
场景 2:动态排序字段
<!-- 根据传入的排序字段动态排序 -->
<select id="selectUsersOrderBy" resultType="User">
SELECT * FROM user
ORDER BY ${orderByColumn} ${sortDirection}
</select>
- 调用方式:
List<User> users = userDao.selectUsersOrderBy("age", "DESC");
- 生成 SQL:
SELECT * FROM user ORDER BY age DESC
场景 3:动态列名(如选择特定字段)
<!-- 选择动态列 -->
<select id="selectDynamicColumns" resultType="map">
SELECT ${columns} FROM user WHERE id = #{id}
</select>
- 调用方式:
Map<String, Object> result = userDao.selectDynamicColumns("name, email", 1001);
- 生成 SQL:
SELECT name, email FROM user WHERE id = ?
3. 安全注意事项
-
风险场景:如果用户输入未经校验,直接使用
${}
可能导致 SQL 注入。// 危险示例:用户输入恶意表名 String tableName = "user; DROP TABLE user; --"; userDao.selectByDynamicTable(tableName, 1001);
生成 SQL:
SELECT * FROM user; DROP TABLE user; -- WHERE id = ?
-
防御措施:
- 白名单校验:限制动态值的范围。
// 只允许特定表名 if (!Arrays.asList("user", "employee").contains(tableName)) { throw new IllegalArgumentException("非法表名"); }
- 转义特殊字符:过滤或转义输入中的特殊符号(如
'
,;
)。
- 白名单校验:限制动态值的范围。
4. 总结
场景 | 占位符 | 示例 | 安全性 |
---|---|---|---|
参数值(如 id ) | #{} | WHERE id = #{id} | 安全(推荐) |
动态表名 | ${} | FROM ${tableName} | 需校验输入(风险) |
动态列名/排序字段 | ${} | ORDER BY ${column} | 需校验输入(风险) |
原则:
- 优先使用
#{}
,确保安全性。 - 仅在必要时使用
${}
,并严格校验输入值。
2. 返回类型
1. 单对象:resultType="User"
- 作用:将单条数据库记录映射到一个 Java 对象(如
User
)。 - 规则:
- 数据库列名需与 Java 对象属性名一致(或通过别名匹配),否则需使用
resultMap
。 - 若查询结果为多条记录,会抛出异常(
TooManyResultsException
)。
- 数据库列名需与 Java 对象属性名一致(或通过别名匹配),否则需使用
- 示例:
<select id="selectUserById" resultType="User"> SELECT * FROM user WHERE id = #{id} </select>
- 返回类型:
User
对象。 - 若未查询到数据,返回
null
。
- 返回类型:
2. 集合:resultType="User"
- 作用:将多条数据库记录映射为
List<User>
。 - 规则:
- MyBatis 自动将多行结果封装为
List
,无需额外配置。 - 若查询结果为空,返回空列表(非
null
)。
- MyBatis 自动将多行结果封装为
- 示例:
<select id="selectAllUsers" resultType="User"> SELECT * FROM user </select>
- 返回类型:
List<User>
。 - 即使结果为空,返回
Collections.emptyList()
。
- 返回类型:
3. Map 类型
3.1 直接返回 Map
(resultType="map"
)
- 作用:将单条记录映射为
Map<String, Object>
,键为列名,值为对应数据。 - 示例:
<select id="selectUserAsMap" resultType="map"> SELECT id, name FROM user WHERE id = #{id} </select>
- 返回类型:
Map<String, Object>
,如{"id": 1, "name": "Alice"}
。
- 返回类型:
3.2 使用 @MapKey
注解返回 Map<K, V>
- 作用:将多条记录映射为
Map<K, V>
,其中:- 键(K):由
@MapKey
指定的属性值(如id
)。 - 值(V):对应的 Java 对象。
- 键(K):由
- 规则:
- 需在 DAO 接口方法上添加
@MapKey("属性名")
。 - 查询结果中指定的属性值必须唯一,否则会覆盖或抛出异常。
- 需在 DAO 接口方法上添加
- 示例:
@MapKey("id") Map<Integer, User> selectAllUsersAsMap();
<select id="selectAllUsersAsMap" resultType="User"> SELECT * FROM user </select>
- 返回类型:
Map<Integer, User>
,键为User
的id
,值为对应的User
对象。
- 返回类型:
4.关键区别与注意事项
类型 | resultType 值 | 返回值类型 | 适用场景 |
---|---|---|---|
单对象 | User | User | 查询单条记录 |
集合 | User | List<User> | 查询多条记录 |
单条记录的 Map | map | Map<String, Object> | 需要灵活访问列名/值的场景 |
多条记录的 Map | User + @MapKey | Map<K, User> | 以特定属性为键,对象为值的映射 |
5.常见问题
-
字段名与属性名不一致
- 使用
resultMap
或 SQL 别名(AS
)解决:<select id="selectUser" resultType="User"> SELECT user_id AS id, user_name AS name FROM user </select>
- 使用
-
集合返回类型为
null
- MyBatis 默认返回空集合(
Collections.emptyList()
),而非null
。
- MyBatis 默认返回空集合(
-
@MapKey
的唯一性- 若指定的键属性(如
id
)存在重复值,MyBatis 会保留最后一个匹配的对象,可能导致数据丢失。
- 若指定的键属性(如
6.总结
- 单对象:直接使用
resultType="User"
。 - 集合:同样使用
resultType="User"
,MyBatis 自动封装为List
。 - Map:
- 单条记录:
resultType="map"
。 - 多条记录:结合
@MapKey
注解,指定键属性。
- 单条记录:
合理选择 resultType
可简化结果映射,提升开发效率。
八、总结
场景 | 解决方案 | 示例 |
---|---|---|
自增主键 | useGeneratedKeys="true" + keyProperty="id" | 插入后自动填充 id |
多参数查询 | @Param 注解 | #{id} + #{name} |
字段名与属性名映射 | resultMap | <result property="userName" column="user_name" /> |
动态条件查询 | <if> + <where> | 按条件拼接 WHERE 子句 |
批量操作 | <foreach> | IN (1, 2, 3) |