MyBatis动态SQL:从条件拼接到底层原理实战

第一部分:动态SQL概述

1.1 为什么需要动态SQL

⭐ 老师强调 :之前在Eclipse里查学生,是在Java中用if-else硬拼接SQL------传名字、性别或班级就手动加条件和空格,很麻烦。现在用动态SQL不用再自己复杂拼接了。

对比 硬拼接 动态SQL
实现方式 代码里人工判断并拼字符串 框架按条件自动处理
易错点 容易漏空格、多and/or 框架自动处理
维护性 麻烦 简洁

⭐ 老师强调:硬拼接是人工判断并拼字符串,易漏空格出错;动态SQL由框架按条件自动处理,更简洁。两者内在联系是都为实现多条件查询,但实现方式从手动变自动。

1.2 参数设计优化

⭐ 老师强调:原本按姓名、性别、年龄分别传参,但接口强制要求三个参数都必须传,调用者不传就编不过。

问题: 想按姓名查也得凑齐性别、年龄。

解决: 改用传一个Student对象代替多个参数------对象里的字段可随意设,没值的就不填。

复制代码
// ❌ 死板写法:必须传三个参数
List<Student> search(String name, String sex, Integer age);

// ✅ 灵活写法:传对象,按需设置字段
List<Student> searchStudent(Student student);

⭐ 老师强调 :传参时若传入Student类型数据,该对象本身肯定传了,但像名字、性别这类字段有无值取决于是否显式设置------设置了才有,没设置就是null。

第二部分:if 标签

2.1 问题场景

⭐ 老师强调 :用实体类查学生时,若参数是null(如age为null),直接拼SQL会写成where age = null,找不到数据,返回零,拼接出错。

问题SQL:

复制代码
select * from student where name = ? and sex = ? and age = ?
-- 若age为null,变成 where age = null,查不到

2.2 if 标签用法

复制代码
<select id="searchStudent" parameterType="com.qcby.entity.Student" 
        resultType="com.qcby.entity.Student">
    select * from student where
    <if test="name!=null and name!=''">
        name = #{name}
    </if>
    <if test="sex!=null and sex!=''">
        and sex = #{sex}
    </if>
    <if test="age!=null and age!=''">
        and age = #{age}
    </if>
</select>

⭐ 老师强调 :if标签类似Java的if判断,但不能写if(条件),而是用test属性写条件。非空才把对应片段拼进语句。

2.3 if 标签的问题

⭐ 老师强调 :若不传name只传sex,拼出where and sex=?,where后直接跟and不合理,SQL语法出错。

问题: 动态拼接要注意首条件不能带and。

⭐ 老师强调:动态拼接要注意首条件不能带and,常需额外处理(如where 1=1或trim前缀)。拼接逻辑对但语义错------不传的字段不进条件没错,但剩余条件若以and开头就破坏语句结构。

第三部分:where 标签

3.1 where if 标签

复制代码
<select id="searchStudent" parameterType="com.qcby.entity.Student" 
        resultType="com.qcby.entity.Student">
    select * from student
    <where>
        <if test="name!=null and name!=''">
            name = #{name}
        </if>
        <if test="sex!=null and sex!=''">
            and sex = #{sex}
        </if>
        <if test="age!=null and age!=''">
            and age = #{age}
        </if>
    </where>
</select>

⭐ 老师强调 :where标签的好处是能按语义自动剔除前方多余and/or------像只传年龄时,它自己把sex前的and裁掉。这类似智能修剪枝叶,只留有效条件,避免语法错。

效果对比:

传参 生成SQL
只传name where name = ?
传sex和age where sex=? and age=?
传name、sex、age where name=? and sex=? and age=?

第四部分:trim 标签

4.1 trim 替代 where

复制代码
<select id="searchStudent" parameterType="com.qcby.entity.Student" 
        resultType="com.qcby.entity.Student">
    select * from student
    <trim prefix="where" prefixOverrides="and | or">
        <if test="name!=null and name!=''">
            name = #{name}
        </if>
        <if test="sex!=null and sex!=''">
            and sex = #{sex}
        </if>
        <if test="age!=null and age!=''">
            and age = #{age}
        </if>
    </trim>
</select>

4.2 trim 属性说明

属性 作用
prefix 前缀,加在整体前面(如where)
prefixOverrides 去掉第一个and或or
suffix 后缀
suffixOverrides 去掉最后一个符号(如逗号)

⭐ 老师强调 :trim标签是更通用的标记 ,能代替set或where标签。prefixOverrides是去掉每句话前边的and/or,suffixOverrides是去掉句末符号。前后去什么都可配置。

第五部分:choose-when-otherwise 标签

5.1 语法结构

复制代码
<select id="searchStudent" parameterType="com.qcby.entity.Student" 
        resultType="com.qcby.entity.Student">
    select * from student
    <where>
        <choose>
            <when test="name!=null and name!=''">
                name = #{name}
            </when>
            <when test="sex!=null and sex!=''">
                and sex = #{sex}
            </when>
            <when test="age!=null and age!=''">
                and age = #{age}
            </when>
            <otherwise>
                and id = 28
            </otherwise>
        </choose>
    </where>
</select>

5.2 与Java if-else对比

MyBatis Java 说明
choose 整体分支结构 包裹所有条件
when if / else if 第一个when是if,后续是else if
otherwise else 都不满足时执行

⭐ 老师强调 :程序会从上到下依次匹配条件 ------先判断第一个条件,满足就只进该分支;不满足再判断第二个;三个都不满足才走otherwise分支。命中即止 ,和if-else if一样。

示例:

传参 执行的SQL
只传age=38 where age = 38
传name和age 匹配到name分支,where name = ?
都不传 走otherwise,where id = 28

第六部分:set 标签(动态修改)

6.1 问题场景

⭐ 老师强调 :上午写法是set name=?, age=?, sex=? where id=?来做修改。三个参数都得传;若少传一个如只改性别,其余字段拼接到SQL里会是空值,把原数据清空了。

问题: 只想改年龄或性别,却清空了名字等原有信息。

6.2 set if 标签

复制代码
<update id="updateStudent" parameterType="com.qcby.entity.Student">
    update student
    <set>
        <if test="name!=null and name!=''">
            name = #{name},
        </if>
        <if test="sex!=null and sex!=''">
            sex = #{sex},
        </if>
        <if test="age!=null and age!=''">
            age = #{age}
        </if>
    </set>
    where id = #{id}
</update>

⭐ 老师强调 :set会自动去掉末尾多余逗号,按实际传参情况决定去留。这样能灵活按需更新,不误清未传字段。

6.3 trim 替代 set

复制代码
<update id="updateStudent" parameterType="com.qcby.entity.Student">
    update student
    <trim prefix="set" suffixOverrides=",">
        <if test="name!=null and name!=''">
            name = #{name},
        </if>
        <if test="sex!=null and sex!=''">
            sex = #{sex},
        </if>
        <if test="age!=null and age!=''">
            age = #{age}
        </if>
    </trim>
    where id = #{id}
</update>

⭐ 老师强调 :trim既能做前缀也能做后缀,可当作一堆if条件的前缀来用。代替set时,prefix="set"加前缀,suffixOverrides=","去掉末尾逗号。

6.4 修改注意事项

⭐ 老师强调:

  • 必须写id条件 :修改时必须写where id=?,否则会误改全表数据
  • 参数莫漏:漏传字段会导致置空
  • 提交事务:改完要提交事务才生效

第七部分:foreach 标签(批量操作)

7.1 批量删除

接口:

复制代码
int deleteStudent(@Param("ids") Integer[] ids);

映射文件:

复制代码
<delete id="deleteStudent">
    delete from student where id in
    <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</delete>

foreach属性说明:

属性 作用 示例
collection 要循环的数组或集合 ids
item 数组中的每一个元素 id
open 循环开始 (
close 循环结束 )
separator 每个元素用什么隔开 ,

生成的SQL:

复制代码
delete from student where id in (5, 6, 7)

⭐ 老师强调:

  • open/close是整体循环的头尾包装,只出现一次
  • separator是每两个元素之间才插入,不参与头尾
  • 多参数或复杂参数必须加@Param注解才能注入

7.2 批量添加

接口:

复制代码
int insertStudents(@Param("students") List<Student> students);

映射文件:

复制代码
<insert id="insertStudents">
    insert into student(name,age,sex) values
    <foreach collection="students" item="stu" separator=",">
        (#{stu.name},#{stu.age},#{stu.sex})
    </foreach>
</insert>

生成的SQL:

复制代码
insert into student(name,age,sex) 
values ('lili456',20,'女'),('lucy456',28,'男'),('tony456',32,'女'),('davi456',21,'男')

⭐ 老师强调:

  • 批量添加本质就是单条insert语句里values后多组括号,用逗号隔开
  • foreach遍历集合,每项拼出一组(#{stu.name},#{stu.age},#{stu.sex})
  • 拼的时候拿集合每一项,用逗号隔开依次造结果,如"项1,项2,项3"
  • 加括号、逗号等修饰都行

第八部分:完整代码汇总

8.1 实体类(Student.java)

复制代码
package com.qcby.entity;

public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String sex;

    public Student() {}

    public Student(String name, Integer age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }

    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    public String getSex() { return sex; }
    public void setSex(String sex) { this.sex = sex; }
}

8.2 接口(StudentDao.java)

复制代码
package com.qcby.dao;

import com.qcby.entity.Student;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface StudentDao {
    // 动态查找
    List<Student> searchStudent(Student student);
    // 动态修改
    int updateStudent(Student student);
    // 批量删除
    int deleteStudent(@Param("ids") Integer[] ids);
    // 批量添加
    int insertStudents(@Param("students") List<Student> students);
}

8.3 映射文件(StudentDao.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.qcby.dao.StudentDao">

    <!-- where if 动态查找 -->
    <select id="searchStudent" parameterType="com.qcby.entity.Student" 
            resultType="com.qcby.entity.Student">
        select * from student
        <where>
            <if test="name!=null and name!=''">
                name = #{name}
            </if>
            <if test="sex!=null and sex!=''">
                and sex = #{sex}
            </if>
            <if test="age!=null and age!=''">
                and age = #{age}
            </if>
        </where>
    </select>

    <!-- trim 动态修改 -->
    <update id="updateStudent" parameterType="com.qcby.entity.Student">
        update student
        <trim prefix="set" suffixOverrides=",">
            <if test="name!=null and name!=''">
                name = #{name},
            </if>
            <if test="sex!=null and sex!=''">
                sex = #{sex},
            </if>
            <if test="age!=null and age!=''">
                age = #{age}
            </if>
        </trim>
        where id = #{id}
    </update>

    <!-- foreach 批量删除 -->
    <delete id="deleteStudent">
        delete from student where id in
        <foreach collection="ids" item="id" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </delete>

    <!-- foreach 批量添加 -->
    <insert id="insertStudents">
        insert into student(name,age,sex) values
        <foreach collection="students" item="stu" separator=",">
            (#{stu.name},#{stu.age},#{stu.sex})
        </foreach>
    </insert>

</mapper>

8.4 测试类(StudentTest.java)

复制代码
package com.qcby;

import com.qcby.dao.StudentDao;
import com.qcby.entity.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class StudentTest {
    private InputStream inputStream = null;
    private SqlSession session = null;
    private StudentDao mapper = null;

    @Before
    public void init() throws IOException {
        inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        session = sqlSessionFactory.openSession();
        mapper = session.getMapper(StudentDao.class);
    }

    @After
    public void destroy() throws IOException {
        session.close();
        inputStream.close();
    }

    // 动态查找
    @Test
    public void run() {
        Student student = new Student();
        // student.setName("张三");
        // student.setAge(38);
        // student.setSex("女");
        List<Student> students = mapper.searchStudent(student);
        for (Student stu : students) {
            System.out.println(stu);
        }
    }

    // 动态修改
    @Test
    public void update() {
        Student student = new Student();
        // student.setName("张三");
        // student.setAge(38);
        student.setSex("男");
        student.setId(28);
        int code = mapper.updateStudent(student);
        session.commit();
        System.out.println(code);
    }

    // 批量删除
    @Test
    public void delete() {
        int code = mapper.deleteStudent(new Integer[]{5, 6, 7});
        session.commit();
        System.out.println(code);
    }

    // 批量添加
    @Test
    public void insert() {
        Student student1 = new Student("lili456", 20, "女");
        Student student2 = new Student("lucy456", 28, "男");
        Student student3 = new Student("tony456", 32, "女");
        Student student4 = new Student("davi456", 21, "男");

        List<Student> students = new ArrayList<Student>();
        students.add(student1);
        students.add(student2);
        students.add(student3);
        students.add(student4);

        int code = mapper.insertStudents(students);
        session.commit();
        System.out.println(code);
    }
}

附录一:动态SQL标签速查表

标签 作用 典型场景
<if> 条件判断,非空才拼 动态查询条件
<where> 自动处理where和多余的and/or 查询条件拼接
<trim> 通用格式化标签,可替代where/set 灵活定制前后缀
<choose>/<when>/<otherwise> 多选一分支 if-else if-else
<set> 动态更新,自动去末尾逗号 修改操作
<foreach> 遍历数组/集合 批量删除、批量添加

trim属性速查

属性 作用
prefix 整体前缀
prefixOverrides 去掉第一个and/or
suffix 整体后缀
suffixOverrides 去掉末尾符号

foreach属性速查

属性 作用
collection 要循环的数组或集合
item 每个元素的临时名
open 循环开始符号
close 循环结束符号
separator 元素间的分隔符
相关推荐
CarIise12 小时前
Spring Boot图片展示与统一返回格式:从资源映射到泛型封装实战
mybatis
钝挫力PROGRAMER1 天前
开发踩坑记:MyBatis selectKey、空 SQL
数据库·sql·mybatis
自强的小白1 天前
核心功能(Service接口)
java·mybatis
自强的小白1 天前
mybatis-plus 的扩展功能
mybatis·mybatis_plus
自强的小白1 天前
MYbatis-plus(自定义sql)
java·sql·mybatis
\光辉岁月/2 天前
6.mybatisplus学习-BaseMapper、Service、常用注解
java·mybatis·mybatisplus
\光辉岁月/2 天前
1.mybatis学习-基础
学习·mybatis
代码什么用2 天前
在WEB中的应用
mybatis
牧瀬クリスだ3 天前
Mybatis-Plus:让CRUD快如闪电
java·spring boot·spring·tomcat·mybatis·yml