MyBatis 动态 SQL 详细指南【完整示例】

发布于:2025-05-13 ⋅ 阅读:(19) ⋅ 点赞:(0)

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 关键字,并移除多余的 ANDOR 运算符,避免拼接 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 语句,满足