前言
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。具体的定义大家可以参考官方文档MyBatis动态SQL。这篇文章我们将结合动态SQL完成更加复杂的 SQL 操作。
增加操作
想必大家肯定遇到过注册某个账号的时候需要输入自己的相关信息,其中这些信息包括:必填信息和非必填信息,对于这些必填信息,我们只需要在创建表的时候将这个字段设置为非 null 就可以了,而对于那些非必选的选项,我们又该如何定义呢?
这时就需要我们使用动态标签来判断了,对于这些可以传递值和可以不传递值的字段,我们可以使用 <if> 标签来修饰:
@Insert("insert into userinfo(username,`password`,age," + "<if test='gender!=null'>gender,</if>" + "phone)" + "values(#{username},#{password},#{age}," + "<if test='gender!=null'>gender,</if>" + "#{phone})") public Integer insertByCondition(UserInfo userInfo);
<if test="">123</if>
这个标签中 test 表示的是判断,当 test 参数中的判断为真时,那么这个标签的结果就为 <if> </if>
标签之间的代码块,在这里就是123;如果 test 中的代码块的判断为假的时候,那么这个<if>
标签的结果就是空。
@Test void insertByCondition() { UserInfo userInfo = new UserInfo(); userInfo.setUsername("彭于晏"); userInfo.setPassword("123456"); userInfo.setAge(18); userInfo.setPhone("132131231"); int ret = userInfoMapper.insertByCondition(userInfo); log.info(ret + "被更新"); }
然后我们调用这个方法的时候,可以不为 gender 字段传递值,如果不传递值,那么这个字段的值就为创建表时定义的默认值,也可以传递值。然后我们运行一下看能达到效果吗?
这里为什么会报错呢?因为 <if>
标签是属于 JavaScript 的,所以我们需要使用到 <script>
标签。
@Insert("<script>" + "insert into userinfo(username,`password`,age," + "<if test='gender!=null'>gender,</if>" + "phone)" + "values(#{username},#{password},#{age}," + "<if test='gender!=null'>gender,</if>" + "#{phone})" + "</script>") public Integer insertByCondition(UserInfo userInfo);
有人会问了,使用 <if> 标签和不使用作用不是一样的吗?对于当前插入数据操作作用是一样的,但是如果我们进行的是修改操作的话,因为我们不知道用户需要修改什么信息,所以我们在写修改操作的话,就需要将所有的字段的修改操作都写上,但是如果我们不使用 <if> 标签的话,并且用户在修改的时候,某个信息没有修改话,后端SQL预处理之后是这样的:update userinfo set username=?, password=?, gender=?, phone=? where username=?,前端传递来的参数是这样的:null, null, null, 123456, 小美,也就是用户只是修改了电话号码这个字段,但是因为没有使用 <if> 标签,所以其他的字段就会被修改为 null,这就会出现问题了。而使用 <if> 标签就会这样处理:update userinfo phone=? where username=?,参数传递:123456, 小美。
上面是使用注解的方式来实现 MyBatis 的,实现 MyBatis 不仅可以通过注解来实现,也可以通过 XML 的格式实现。我们看看 XML 如何实现动态 SQL。
首先我们需要告诉 MyBatis 我们的 xml 文件在哪里:
mybatis: mapper-locations: classpath:mapper/**Mapper.xml
然后在 XML 文件中写入下面代码,SQL 语句写在 <mapper>
标签中。
<?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.example.mybatis20240101.mapper.UserInfoMapper"> </mapper>
这里 namespace 的值是我们是使用了 MyBatis 框架操作数据库的类的全限定类名称。
<insert id="insertByCondition"> insert into userinfo(username,`password`,age, <if test="gender!=null"> gender, </if> phone) values(#{username},#{password},#{age}, <if test="gender!=null"> #{gender}, </if> #{phone}) </insert>
因为 XML 文件本身就支持 JavaScript,所以我们这里不需要添加 <script>
标签,不仅如此,使用 XML 文件的方式写 MyBatis 还会有提示,所以书写动态 SQL 建议使用 XML 文件格式。
但是使用 <if>
标签也会存在问题,假设我们将 phone 字段也设置为动态的,并且在传递值的时候不传递 phone 的话就会出现问题。
<insert id="insertByCondition"> insert into userinfo(username,`password`,age, <if test="gender!=null"> gender, </if> <if test="phone!=null"> phone </if>) values(#{username},#{password},#{age}, <if test="gender!=null"> #{gender}, </if> <if test="phone!=null"> #{phone} </if>) </insert>
@Test void insertByCondition() { UserInfo userInfo = new UserInfo(); userInfo.setUsername("彭于晏"); userInfo.setPassword("123456"); userInfo.setAge(18); //userInfo.setPhone("123456"); int ret = userInfoMapper.insertByCondition(userInfo); log.info(ret + "被更新"); }
可以看到,因为 phone 是我们定义增加操作时候的最后一个字段,在这里将 phone 设置为动态的,并且在传递值的时候没有传递 phone,那么在拼接 SQL 的时候 insert into userinfo() values() 中两个括号中一定会是以逗号结尾,那么这样的 SQL 就是不和规则的 SQL,那么如何解决呢?这里就需要用到 <trim> 标签了。
<trim>标签
trim 标签在 MyBatis 中主要用于处理 SQL 语句,以去除多余的关键字、逗号,或者添加特定的前缀和后缀。这在进行选择性插入、更新、删除或条件查询等操作时非常有用。
trim 标签有以下属性:
- prefix:表⽰整个语句块,以prefix的值作为前缀
- suffix:表⽰整个语句块,以suffix的值作为后缀
- prefixOverrides:表⽰整个语句块要去除掉的前缀
- suffixOverrides:表⽰整个语句块要去除掉的后缀
因为我们这里是语句块末尾出现了多余的逗号,所以我们配置 suffixOverrides 属性来删除多余的逗号。
<insert id="insertByCondition"> insert into userinfo <trim prefix="(" suffix=")" suffixOverrides=","> username,`password`,age, <if test="gender!=null"> gender, </if> <if test="phone!=null"> phone </if> </trim> values <trim prefix="(" suffix=")" suffixOverrides=","> #{username},#{password},#{age}, <if test="gender!=null"> #{gender}, </if> <if test="phone!=null"> #{phone} </if> </trim> </insert>
查询操作
大家在肯定在网上买过手机吧,当我们买手机的时候,往往会加上一些限制条件。
而这种加上限制条件的查询就可以看成是 select 后面加了 where 语句,但是由于不知道用户需要加上多少查询时候的限制条件,所以这里就可以使用到动态 SQL。
UserInfo selectByCondition(UserInfo userInfo);
<select id="selectByCondition" resultType="com.example.mybatis20240101.model.UserInfo"> select * from userinfo where <if test="username!=null"> username=#{username} </if> <if test="password!=null"> and password=#{password} </if> <if test="age!=null"> and age=#{age} </if> <if test="phone!=null"> and phone=#{phone} </if> </select>
@Test void selectByCondition() { UserInfo userInfo = new UserInfo(); userInfo.setUsername("彭于晏"); userInfo.setPhone("34567"); log.info(userInfoMapper.selectByCondition(userInfo).toString()); }
当然这里也会出现问题,就是当第一个 where 子句没有传递值的话,那么 where 子句中就会多一个 and 在开头。
@Test void selectByCondition() { UserInfo userInfo = new UserInfo(); //userInfo.setUsername("彭于晏"); userInfo.setPhone("34567"); log.info(userInfoMapper.selectByCondition(userInfo).toString()); }
为了解决问题,可以使用 <trim>
标签删除前面多余的 and:
<select id="selectByCondition" resultType="com.example.mybatis20240101.model.UserInfo"> select * from userinfo where <trim prefixOverrides="and"> <if test="username!=null"> username=#{username} </if> <if test="password!=null"> and password=#{password} </if> <if test="age!=null"> and age=#{age} </if> <if test="phone!=null"> and phone=#{phone} </if> </trim> </select>
<where>标签
这里是解决了 where 子句中开头出现多余的 and,如果我们一个限制条件都不加入呢?
@Test void selectByCondition() { UserInfo userInfo = new UserInfo(); //userInfo.setUsername("彭于晏"); //userInfo.setPhone("34567"); log.info(userInfoMapper.selectByCondition(userInfo).toString()); }
这样就会出现问题,那么这样如何解决呢?MyBatis 为我们提供了 <where>
标签用来解决查询语句的 where 子句出现的各种问题,包括开头出现的多余的 and 和 where 子句无内容的情况。
<select id="selectByCondition" resultType="com.example.mybatis20240101.model.UserInfo"> select * from userinfo <where> <if test="username!=null"> username=#{username} </if> <if test="password!=null"> and password=#{password} </if> <if test="age!=null"> and age=#{age} </if> <if test="phone!=null"> and phone=#{phone} </if> </where> </select>
可以看到,当我们 where 子句为空的时候,使用 <where>
标签会自动删除 where 子句,它也可以帮助我们删除多余的 and,下面的报错咱们不管,这时因为我们没加 where 子句,所以就相当于查询整个表,但是我们方法的返回值是 UserInfo,改为列表就可以了。
当 where 子句为空的时候,我们还有一种解决方式,就是自己加上一个 1=1 的条件,这样当我们加的条件为空的时候就不会出现错误。
<select id="selectByCondition" resultType="com.example.mybatis20240101.model.UserInfo"> select * from userinfo where 1=1 <trim prefixOverrides="and"> <if test="username!=null"> username=#{username} </if> <if test="password!=null"> and password=#{password} </if> <if test="age!=null"> and age=#{age} </if> <if test="phone!=null"> and phone=#{phone} </if> </trim> </select>
修改操作
这个修改操作就是前面我们举的一个例子,我们并不知道用户会修改哪些信息,所以我们这里就需要使用到动态 SQL 来解决这个问题。
void updateByCondition(UserInfo userInfo);
@Test void updateByCondition() { UserInfo userInfo = new UserInfo(); userInfo.setUsername("小美"); userInfo.setPassword("666666"); userInfo.setPhone("23456"); userInfoMapper.updateByCondition(userInfo); }
<set>标签
为了解决 set 中出现的 set 子句中多余的逗号的问题,可以使用 <set>
标签。
<update id="updateByCondition"> update userinfo <set> <if test="password!=null"> password=#{password}, </if> <if test="age!=null"> age=#{age}, </if> <if test="gender!=null"> gender=#{gender}, </if> <if test="phone!=null"> phone=#{phone} </if> </set> <where> <if test="username!=null"> username=#{username} </if> </where> </update>
如果 set 子句为空的时候,是会报错的,通过 <set>
标签无法解决,我们应该避免用户 set 输入空的子句。
删除操作
<foreach>标签
当我们想要在删除的时候删除不定数量的条件时,delete from userinfo where name in("小美","小帅"),因为条件的数量是不确定的,所以我们在定义的时候就不知道 where 子句后面有多少个,所以这里我们就需要用到 <foreach> 标签。
<foreach> 标签可以对集合进行遍历,标签具有以下属性:
- collection:绑定⽅法参数中的集合,如 List,Set,Map或数组对象
- item:遍历时的每⼀个对象
- open:语句块开头的字符串
- close:语句块结束的字符串
- separator:每次遍历之间间隔的字符串
void deleteByCondition(List<Integer> list);
<delete id="deleteByCondition"> delete from userinfo where id in <foreach collection="list" item="id" open="(" close=")"> #{id} </foreach> </delete>
@Test void deleteByCondition() { userInfoMapper.deleteByCondition(Arrays.asList(13,18,19,20)); }
<include>标签
在xml映射⽂件中配置的SQL,有时可能会存在很多重复的⽚段,此时就会存在很多冗余的代码
我们可以对重复的代码⽚段进⾏抽取,将其通过 <sql>
标签封装到⼀个SQL⽚段,然后再通过
<include>
标签进⾏引⽤。
ArrayList<UserInfo> selectAll();
<sql id="allColumn"> id, username, age, gender, phone, delete_flag, create_time, update_time </sql>
<select id="selectAll" resultType="com.example.mybatis20240101.model.UserInfo"> select <include refid="allColumn"></include> from userinfo </select>
@Test void selectAll() { log.info(userInfoMapper.selectAll().toString()); }
这样就可以减少很多重复的代码。