MyBatis 动态 SQL 详细指南
一、数据库表结构
创建一个名为 users
的用户表,表结构如下:
CREATE TABLE IF NOT EXISTS users
(
user_id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
user_name varchar(128) NULL COMMENT '用户名',
user_age int COMMENT '用户年龄',
user_status varchar(32) COMMENT '用户状态',
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
PRIMARY KEY (user_id)
) COMMENT '用户表' COLLATE = utf8mb4_unicode_ci;
二、MyBatis 基础元素
1. Java Bean 的 ORM 映射对象
package com.example.entity;
public class User {
private Long id;
private String name;
private Integer age;
private String status;
private java.util.Date createTime;
// 省略 Get、Set 方法
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", status='" + status + '\'' +
", createTime=" + createTime +
'}';
}
}
2. <mapper>
标签
<mapper>
是 MyBatis 映射文件的根标签,通过 namespace
属性指定对应的 Mapper 接口全限定名,实现 XML 映射文件与 Java 接口的关联。示例如下:
<mapper namespace="com.example.dao.UserMapper">
<!-- 具体的 SQL 语句和配置会写在这个标签内部,通过 namespace 建立与接口的联系 -->
</mapper>
3. <resultMap>
标签
用于定义数据库表字段和 Java 对象属性的映射关系,尤其在表字段名与对象属性名不一致时非常有用。对应的 resultMap
配置如下:
<resultMap id="UserResultMap" type="com.example.entity.User">
<!-- 主键映射,property 对应实体类属性名,column 对应数据库表中的列名 -->
<id property="id" column="user_id"/>
<!-- 普通字段映射,property 对应实体类属性名,column 对应数据库表中的列名 -->
<result property="name" column="user_name"/>
<result property="age" column="user_age"/>
<result property="status" column="user_status"/>
<result property="createTime" column="create_time"/>
</resultMap>
4. <sql>
标签
用于定义可复用的 SQL 片段,提高代码的可维护性和复用性。通过 <include>
标签引用这些片段,示例如下:
<sql id="Base_Column_List">
user_id, user_name, user_age, user_status, create_time
</sql>
<select id="selectUserById" resultMap="UserResultMap">
SELECT
<include refid="Base_Column_List"/> <!-- 引入可复用的 SQL 片段 -->
FROM users
WHERE user_id = #{id}
</select>
三、Mapper 接口定义
package com.example.dao;
import com.example.entity.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
User selectUserById(Long id);
List<User> selectUsersByCondition(User user);
int batchInsertUsers(List<User> userList);
int updateUser(User user);
}
四、核心动态 SQL 标签详解
1. 条件控制类标签
<if>
标签:根据条件判断决定 SQL 片段是否包含在最终的 SQL 语句中。常用于根据不同的参数值动态生成 SQL。示例:
<select id="selectUsersByCondition" resultMap="UserResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
<where>
<!-- 当 name 不为空且不为空字符串时,添加该条件 -->
<if test="name != null and name != ''">
AND user_name = #{name}
</if>
<!-- 当 age 不为空时,添加该条件 -->
<if test="age != null">
AND user_age = #{age}
</if>
<!-- 当 status 不为空且不为空字符串时,添加该条件 -->
<if test="status != null and status != ''">
AND user_status = #{status}
</if>
</where>
</select>
<choose>
、<when>
和<otherwise>
标签:类似于 Java 中的switch - case
语句,根据不同的条件选择不同的 SQL 片段。只会执行第一个满足条件的<when>
标签中的 SQL 片段,若所有<when>
条件都不满足,则执行<otherwise>
标签中的 SQL 片段。示例:
<select id="selectUsersByTime" resultMap="UserResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
<where>
<choose>
<!-- 当 startTime 和 endTime 都不为空时,添加该条件到 SQL 语句中 -->
<when test="startTime != null and endTime != null">
AND create_time BETWEEN #{startTime} AND #{endTime}
</when>
<!-- 当 startTime 不为空且 endTime 为空时,添加该条件到 SQL 语句中 -->
<when test="startTime != null and endTime == null">
AND create_time = #{startTime}
</when>
<!-- 当上述条件都不满足时,添加该条件到 SQL 语句中 -->
<otherwise>
AND YEAR(create_time) = #{year}
</otherwise>
</choose>
</where>
</select>
2. 语法修正类标签
<where>
标签:自动处理 SQL 语句中的WHERE
关键字,并移除多余的AND
或OR
运算符,避免拼接 SQL 时出现语法错误。示例:
<select id="selectUsersByCondition" resultMap="UserResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
<where>
<!-- 当 name 不为空且不为空字符串时,添加该条件,where 标签会自动处理开头的 AND -->
<if test="name != null and name != ''">
AND user_name = #{name}
</if>
<!-- 当 age 不为空时,添加该条件,where 标签会自动处理与上一个条件之间的 AND -->
<if test="age != null">
AND user_age = #{age}
</if>
</where>
</select>
<trim>
标签:提供更灵活的语法修正功能,可替代<where>
或<set>
标签。可以自定义前缀、后缀以及需要删除的字符。示例:
<trim prefix="WHERE" prefixOverrides="AND |OR">
<!-- 当 name 不为空时,添加该条件,trim 标签会根据配置处理前缀和多余字符 -->
<if test="name != null">AND name=#{name}</if>
</trim>
<set>
标签:自动处理UPDATE
语句的SET
子句,动态更新字段并去除末尾逗号。示例:
<update id="updateUser" parameterType="com.example.entity.User">
UPDATE users
<set>
<!-- 当 name 不为空时,添加该更新字段,set 标签会自动处理末尾逗号 -->
<if test="name != null">name = #{name},</if>
<!-- 当 age 不为空时,添加该更新字段,set 标签会自动处理与上一个字段之间的逗号 -->
<if test="age != null">age = #{age}</if>
<if test="status != null">status = #{status}</if>
</set>
WHERE id = #{id}
</update>
3. 集合处理类标签
<foreach>
标签:用于遍历集合,常用于批量操作或根据集合元素动态生成 SQL 语句。例如批量插入数据:
<insert id="batchInsertUsers" parameterType="java.util.List">
INSERT INTO users (user_name, user_age, user_status, create_time)
VALUES
<!-- collection 指向传入的集合参数,item 表示集合中的每个元素,separator 指定元素之间的分隔符 -->
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.status}, #{user.createTime})
</foreach>
</insert>
<bind>
标签:预定义变量,在模糊查询等场景中非常有用。示例:
<select id="selectUsersByPartialName" resultMap="UserResultMap">
<!-- 绑定一个名为 namePattern 的变量,其值为拼接后的模糊查询字符串 -->
<bind name="namePattern" value="'%' + name + '%'"/>
SELECT
<include refid="Base_Column_List"/>
FROM users
<where>
user_name LIKE #{namePattern}
</where>
</select>
五、参数引用
在 SQL 语句中使用 #{}
语法引用传入参数的属性。MyBatis 会对参数进行预编译处理,有效防止 SQL 注入攻击。示例:
<select id="selectUserById" resultMap="UserResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
WHERE user_id = #{id} <!-- #{id} 引用传入的 id 参数,进行预编译处理 -->
</select>
六、常见查询场景处理
1. 范围查询
当需要根据某个字段的范围进行查询时,使用 BETWEEN
关键字结合动态 SQL 实现。示例:
<select id="selectUsersByTimeRange" resultMap="UserResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
<where>
<!-- 当 startTime 和 endTime 都不为空时,添加范围查询条件 -->
<if test="startTime != null and endTime != null">
AND create_time BETWEEN #{startTime} AND #{endTime}
</if>
</where>
</select>
2. 精确查询
根据某个字段的精确值进行查询时,直接使用 =
进行比较。示例:
<select id="selectUserByName" resultMap="UserResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
<where>
<!-- 当 name 不为空且不为空字符串时,添加精确查询条件 -->
<if test="name != null and name != ''">
AND user_name = #{name}
</if>
</where>
</select>
3. 模糊查询
使用 LIKE
关键字结合 CONCAT
函数进行模糊查询。示例:
<select id="selectUsersByPartialName" resultMap="UserResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
<where>
<!-- 当 partialName 不为空且不为空字符串时,添加模糊查询条件 -->
<if test="partialName != null and partialName != ''">
AND user_name LIKE CONCAT('%', #{partialName}, '%')
</if>
</where>
</select>
4. 字符串列表匹配
使用 FIND_IN_SET
函数判断一个字符串是否存在于以逗号分隔的字符串列表中。示例:
<select id="selectUsersByRoleIds" resultMap="UserResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM users
<where>
<!-- 当 roleIds 不为空且不为空字符串时,添加字符串列表匹配条件 -->
<if test="roleIds != null and roleIds != ''">
AND FIND_IN_SET(#{roleIds}, user_role_ids)
</if>
</where>
</select>
七、性能优化与最佳实践
1. OGNL 表达式优化
避免在 XML 中使用过于复杂的 OGNL 表达式,优先在 Java 层处理判断逻辑,以提高性能和代码可读性。例如:
<if test="@com.utils.MyBatisUtils@isValid(name)">
2. 批量操作优化
- 使用
BATCH
模式执行SqlSession
,减少数据库连接开销。 - 对于超大数据集,分批次处理数据,如每 1000 条提交一次,避免一次性处理大量数据导致性能问题。
3. 缓存策略
- 二级缓存:在 Mapper 级别配置二级缓存,实现跨会话共享缓存数据。示例:
<mapper namespace="com.UserMapper">
<cache eviction="LRU" flushInterval="60000"/>
</mapper>
- 注意脏读问题:在执行更新操作后,手动清除缓存,确保数据一致性。例如:
<update id="updateUser" flushCache="true">
八、安全与规范
1. 防 SQL 注入
禁止直接拼接 ${}
参数(除非绝对可信),应使用 #{}
预编译参数化查询,防止 SQL 注入攻击。
2. 参数校验
- 集合判空:使用
<if test="list != null and!list.isEmpty()">
判断集合是否为空,避免空指针异常。 - 字符串处理:使用
<if test="name != null and name.trim() != ''">
处理字符串,确保字符串不为空且去除前后空格。
3. 类型安全
显式声明 jdbcType
,避免 NULL 类型推断错误。例如:
#{age,jdbcType=INTEGER}
九、高级特性
1. 注解动态 SQL
通过注解方式实现动态 SQL,提高代码的简洁性和可读性。示例:
@SelectProvider(type = UserSqlBuilder.class, method = "buildQuery")
List<User> findUsers(UserQuery query);
2. 动态表名映射
在 SQL 语句中动态指定表名,但需注意潜在的 SQL 注入风险。示例:
<select id="selectByTable" resultType="map">
SELECT * FROM ${tableName}
</select>
十、使用示例
import com.example.dao.UserMapper;
import com.example.entity.User;
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.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class MyBatisDynamicSQLExample {
public static void main(String[] args) {
try {
// 加载 MyBatis 配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取 SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 根据 ID 查询用户
User userById = userMapper.selectUserById(1L);
System.out.println("根据 ID 查询用户: " + userById);
// 根据条件查询用户
User userCondition = new User();
userCondition.setName("John");
userCondition.setAge(30);
List<User> usersByCondition = userMapper.selectUsersByCondition(userCondition);
System.out.println("根据条件查询用户: " + usersByCondition);
// 批量插入用户
List<User> userList = new ArrayList<>();
User user1 = new User();
user1.setName("Alice");
user1.setAge(25);
user1.setStatus("active");
User user2 = new User();
user2.setName("Bob");
user2.setAge(28);
user2.setStatus("active");
userList.add(user1);
userList.add(user2);
int batchInsertCount = userMapper.batchInsertUsers(userList);
System.out.println("批量插入用户数量: " + batchInsertCount);
// 更新用户
User updateUser = userMapper.selectUserById(1L);
updateUser.setName("Updated Name");
int updateCount = userMapper.updateUser(updateUser);
System.out.println("更新用户数量: " + updateCount);
// 提交事务
sqlSession.commit();
// 关闭 SqlSession
sqlSession.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
以上示例展示了如何使用 MyBatis 动态 SQL 进行常见的数据库操作,包括查询、插入和更新。通过合理运用 MyBatis 的动态 SQL 标签,可以灵活地构建 SQL 语句,满足