12. MyBatis(二)

源码位置:MyBatis_demo

上篇文章我们学习了MyBatis的定义以及增删查改操作,并且学习了如何在xml文件中编写SQL时使用#{}的方式将参数和对象的属性映射到SQL语句中,上篇的内容已经足以应对大部分场景,本篇文章我们就要学习一下MyBatis的进阶操作,拿捏MyBatis实战。

1. 参数占位符 ------ #{} 和 ${}

MyBatis中有两种参数占位符,分别是"#{}""${}",上节课我们学习了"#{}"的使用,以上节课根据id查询用户的业务为例:

接口以及xml文件中的SQL实现如下:

java 复制代码
public UserInfo getUserById(@Param("uid") Integer id);

把xml文件中的"#{}"改为"${}"

xml 复制代码
<select id="getUserById" resultType="com.chenshu.mybatis_demo.model.UserInfo">
    select * from userinfo where id = ${uid}
</select>

调用测试方法后成功查到数据:

我们发现将"#{}"改为"${}"后同样能够查出数据,那不是玩我呢吗?

不要着急,这时我们再编写一个通过username查询的业务,接口以及xml文件中的SQL实现如下:

java 复制代码
public List<UserInfo> getUserByUsername(@Param("username") String username);
xml 复制代码
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
    select * from userinfo where username = #{username}
</select>

单元测试:

java 复制代码
@Test
void getUserByUsername() {
    List<UserInfo> userInfoList = userMapper.getUserByUsername("zhangsan");
    System.out.println(userInfoList);
}

在使用"#{}"参数占位符的情况下成功取到数据:

改为"${}"后居然报错了,报错信息:在where子句中有未知的列'zhangsan'

为什么呢会这样呢?我明明是想要查询usernamezhangsan的列呀,他为什么说没找到名字为"zhangsan"的列呢?原因很简单,两种占位符的区别如下:

1.1 #{}:预编译处理

#{}是预编译处理,等同于JDBC中的"?"占位符,预编译的SQL语句不是有具体数值的语句,而是用(?)来代替具体数据,然后在执行的时候再调用setXXX()方法把具体的数据传入。

xml 复制代码
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
    select * from userinfo where username = #{username}
</select>

预编译的处理下,在数据库中执行的SQL语句为:select * from userinfo where username = "zhangsan";也就是查询usernamezhangsan的数据。

1.2 ${}:字符直接替换

${}是字符直接替换,会直接将传入参数的值放入SQL语句中。

xml 复制代码
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
    select * from userinfo where username = ${username}
</select>

在数据库中执行的SQL语句为:select * from userinfo where username = zhangsan;,这时就变成了在表中查找出所有userinfozhangsan这两个字段值相同的数据;

了解后了两者的差异后,我们直接将xml中的SQL改为下面的形式:

xml 复制代码
<select id="getUserByUsername" resultType="com.chenshu.mybatis_demo.model.UserInfo">
    select * from userinfo where username = '${username}'
</select>

也成功查询到了数据:

1.3 ${}的缺陷:SQL注入

以登录功能为例,正常情况下,用户在前端需要传一个账号和密码给后端,后端再把账号和密码放到where子句中查询数据库,如果查到对应的数据,就证明用户名和密码输入正确,登录成功,SQL语句如下:

sql 复制代码
select * from userinfo where username = 'zhangsan' and password = '123';

输入正确就能查询到数据:

否则就查不到数据:

这时我们来使用${}实现一下登录功能:

接口:

java 复制代码
public UserInfo userLogin(@Param("username") String username, @Param("password") String password);

xml编写SQL:

xml 复制代码
<select id="userLogin" resultType="com.chenshu.mybatis_demo.model.UserInfo">
    select * from userinfo where username = '${username}' and password = '${password}'
</select>

单元测试:

java 复制代码
@Test
void userLogin() {
    UserInfo userInfo = userMapper.userLogin("zhangsan", "123");
    if (userInfo == null) {
        System.out.println("用户名或密码错误,请重新登录!");
    } else {
        System.out.println("登录成功!");
        System.out.println(userInfo);
    }
}

在输入正确的账号和密码后,打印了下面信息:

输入错误的密码后,打印了下面信息:

1.3.1 SQL注入

当有别有用心之人在其不知道密码的情况下,恶意利用SQL语句的特性登录成功,就是SQL注入,下面给大家举个例子:

SQL注入代码:"' or 1='1"

将传入的密码改为上面的SQL注入语句:

java 复制代码
@Test
void userLogin() {
    UserInfo userInfo = userMapper.userLogin("zhangsan", "' or 1='1");
    if (userInfo == null) {
        System.out.println("用户名或密码错误,请重新登录!");
    } else {
        System.out.println("登录成功!");
        System.out.println(userInfo);
    }
}

居然真的查询到相应字段了,该用户也就登录成功了:

为什么会出现这种情况呢?

原因是将密码写为' or 1='1传入数据库查询时,语句就变成了下面这样:

此时不管你输入的密码是什么,都会查询出表中的所有数据,这就是SQL注入。

如果是预编译的话,数据库就只会把' or 1='1当作一整个字符串去处理,而不会把它当作SQL脚本去处理,也就不会出现SQL注入的问题,我们现在将xml文件中的SQL改写为#{}的形式:

xml 复制代码
<select id="userLogin" resultType="com.chenshu.mybatis_demo.model.UserInfo">
    select * from userinfo where username = '${username}' and password = #{password}
</select>

查不到任何数据,登录失败!

1.4 ${}的使用场景

1.4.1 排序查询

既然预处理这么好用,为什么会存在两种占位符呢?存在即合理,有些特殊情况就是需要使用"${}"来处理的。

当后端需要传一些关键字,比如专栏中给用户提供了选择通过数据库的某一列进行排序 文章的功能,实现这个功能就需要使用${}来完成

这时我们在数据库中就需要使用order by xxx作为排序的依据,如果使用"#{}"的方式,由于后端传入了一个String,就会预编译order by 'xxx'

因此此时需要使用${}来实现排序查询,具体的接口与SQL实现如下:

java 复制代码
public List<ArticleInfo> sortArticle(@Param("sort") String sort);
xml 复制代码
<select id="sortArticle" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
    select * from articleinfo order by ${sort}
</select>

单元测试:根据createtime字段排序

java 复制代码
@Test
void sortArticle() {
    List<ArticleInfo> articleList= articleMapper.sortArticle("createtime");
    for (ArticleInfo articleInfo : articleList) {
        System.out.println(articleInfo);
    }
}

运行结果:成功根据createtime字段的升序打印所有文章列表

1.4.2 like 模糊查询

a) 使用${}

like 使用 #{} 会报错,因为预编译后的SQL如下:

这时我们就可以使用${},接口以及实现如下:

java 复制代码
public List<ArticleInfo> searchArticle(@Param("title") String title);
xml 复制代码
<select id="searchArticle" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
    select * from articleinfo where title like '%${title}%'
</select>

单元测试:

java 复制代码
@Test
void searchArticle() {
    List<ArticleInfo> articleList = articleMapper.searchArticle("MyBatis");
    for (ArticleInfo articleInfo : articleList) {
        System.out.println(articleInfo);
    }
}

成功查询到 title 中带 'MyBatis' 的数据:

b) 使用concat()

concat()是mysql内置函数,可以用作字符串的拼接:

修改xml中的SQL语句:

xml 复制代码
<select id="userLogin" resultType="com.chenshu.mybatis_demo.model.UserInfo">
    select * from userinfo where username = '${username}' and password = #{password}
</select>

再次进行单元测试:

2. resultMap的使用

上一篇文章我们提到过resultType的使用,作用是将数据库返回的记录和类映射起来,但是当类的属性名与数据库中的列名不同时,就会出现无法映射的问题,这里我来举个例子:

将UserInfo原来的属性名username修改为name:

java 复制代码
@Data
public class UserInfo {
    private int id;
    private String username;
    private String password;
    private String photo;
    private Timestamp createTime;
    private Timestamp updateTime;
    private int state;
}

此时再调用一下单元测试的getAll()方法,发现name的值为null:

这里我们就需要使用resultMap定义一个映射规则来解决字段名和属性名不同的情况:

xml 复制代码
<resultMap id="userInfoMap" type="com.chenshu.mybatis_demo.model.UserInfo">
    <id column="id" property="id"/>
    <result column="username" property="name"/>
</resultMap>

解释下上面的标签和属性分别代表什么:

  • resultMap标签中的id就是你给该映射规则自定义的名称;
  • resultMap标签中的type就对应着你需要映射的类;
  • id标签是用来映射主键的,使用上推荐不管表中主键字段名和映射类的的属性是否相同,都配置这一项,否则多表查询的时候会出问题;
  • result标签是用来映射普通字段和映射类的属性的
  • idresult标签中都有一个columnproperty属性,是用来配置映射的,分别对应表的字段名类的属性名

getAll()方法对应的SQL中的resultType改为resultMap

xml 复制代码
<select id="getAll" resultMap="userInfoMap">
    select * from userinfo
</select>

再次测试,成功拿到了name(对应数据库中的username)的值:

3. 多表查询

  1. 连接表需要查询的字段添加到实体类
  1. 编写接口的getAll()方法以及xml文件中的SQL
java 复制代码
public List<ArticleInfo> getAll();
xml 复制代码
<select id="getAll" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
    select a.*, u.username from articleinfo as a
    left join userinfo as u
    on a.uid = u.id
</select>
  1. 单元测试并得到结果
java 复制代码
@Test
void getAll() {
    List<ArticleInfo> list = articleMapper.getAll();
    for (ArticleInfo articleInfo : list) {
        System.out.println(articleInfo);
    }
}

4. 动态SQL

动态SQL是MyBatis的强大特性之一,能够完成不同条件下不同的SQL拼接。

为什么要使用动态SQL呢?给大家引入一个案例:

在注册用户的时候,可能分为必填字段非必填字段,如果添加用户的时候有不确定的字段传入,程序应该如何实现呢?

这时候就有小伙伴提到:大不了多写几个方法让他们重载呗。

这种方式十分不优雅:如果是一个选填字段的话,就需要去写两个方法,两个选填字段的话就要写四个方法,如果有n个选填字段,那么就需要写2^n个方法...

因此我们就需要要使用MyBatis的重要特性 ------ 动态SQL,接下来我们就来讲解一下常用的动态SQL标签的使用。

在讲解动态SQL标签之前可以在配置文件中添加一下log-impl用于打印SQL语句,以便对比:

4.1 <if>标签

前面提到的案例就要使用<if>动态标签来判断了。

这里我们来具体使用下<if>标签,比如用户在注册的时候,必填字段为usernamepassword,选填字段为photo

表结构如下:photo字段默认值为'';

4.1.1 不使用<if>标签

java 复制代码
public int add(UserInfo userInfo);
xml 复制代码
<insert id="add">
    insert into userinfo(username, photo, password)
    values (
            #{name},
            #{photo},
            #{password})
</insert>

单元测试代码:

java 复制代码
@Test
void add() {
    UserInfo userInfo = new UserInfo();
    userInfo.setName("zhangsan");
    userInfo.setPassword("123");
    userMapper.add(userInfo);
}

日志信息如下:我们发现SQL语句被预编译成了Preparing: insert into userinfo(username, photo, password) values ( ?, ?, ?),并且photo字段传入了一个null

==>  Preparing: insert into userinfo(username, photo, password) values ( ?, ?, ?)
==>  Parameters: zhangsan(String), null, 123(String)
<==  Updates: 1

表中插入了如下字段:


4.1.2 使用<if>标签

需要在test属性内写判断的语句,如果"photo != null",那么就拼接if标签里面的内容,否则就不拼接:

xml 复制代码
<insert id="add">
    insert into userinfo(
                    username,
                    <if test="photo != null">
                        photo,
                    </if>
                    password
                    )
    values (
        #{name},
        <if test="photo != null">
            #{photo},
        </if>
        #{password}
    )
</insert>

插入一条"lisi"的数据(不带photo值):

java 复制代码
@Test
void add() {
    UserInfo userInfo = new UserInfo();
    userInfo.setName("lisi");
    userInfo.setPassword("123");
    userMapper.add(userInfo);
}

日志信息如下:由于没有传入photo参数,因此SQL语句此时被编译成了insert into userinfo( username, password ) values ( ?, ? ),并且只传入了usernamepassword两个参数

==>  Preparing: insert into userinfo( username, password ) values ( ?, ? )
==>  Parameters: lisi(String), 123(String)
<==  Updates: 1

对比表中的两条数据,我们发现在不使用if标签的时候photo插入的值为null,使用if标签的时候photo的值为默认值''

使用if标签的意义: 由于NULL''在MySQL中是不同的,因此你要使用一个查询语句select * from userinfo where photo = '';,就无法查询到zhangsan这条记录:

甚至当photo标签的字段上加了nou null约束的话,在不使用if标签的情况下会直接报错

4.2 <trim>标签

前面我们在使用if标签的时候选择了使用一种取巧的方式,把使用if标签的地方放在参数的中间位置,如果位于最后面的话,会出现问题,像下面这样:

xml 复制代码
<insert id="add">
    insert into userinfo(
                    username,
                    password,
                    <if test="photo != null">
                        photo
                    </if>
                    )
    values (
        #{name},
        #{password},
        <if test="photo != null">
            #{photo}
        </if>
    )
</insert>

如果photo=null的话,不拼接photo的内容,此时的SQL语句就变成了insert into userinfo( username, password, ) values ( ?, ?, ),由于最后一个参数不能以逗号结尾,因此SQL会报语法错误:

这时候我们就需要去使用一个trim标签去操作,

<trim>标签中有如下属性:

  • prefix:表示整个语句块,以prefix的值为前缀
  • suffix:表示整个语句块,以suffix的值作为后缀
  • prefixOverrides:表示整个语句块要去除掉的前缀
  • suffixOverrides:表示整个语句块要去除掉的后缀

假设我们三个参数(username, password, photo)都是可选参数,如何巧妙地使用trim标签来处理呢?

改造xml文件中的SQL:使用trim标签的prefixsuffix以及suffixOverrides参数

xml 复制代码
<insert id="add">
    insert into userinfo
                    <trim prefix="(" suffix=")" suffixOverrides=",">
                        <if test="name != null">
                            username,
                        </if>
                        <if test="password != null">
                            password,
                        </if>
                        <if test="photo != null">
                            photo
                        </if>
                    </trim>
    values
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="name != null">
                #{name},
            </if>
            <if test="password != null">
                #{password},
            </if>
            <if test="photo != null">
                #{photo}
            </if>
        </trim>
</insert>

这里我需要多提一嘴,如果trim标签里的所有if语句都不生效,就不会添加前后缀,而是直接编译成:insert into userinfo values,因此在使用时至少传入一个参数

单元测试:

java 复制代码
@Test
void add() {
    UserInfo userInfo = new UserInfo();
    userInfo.setName("wangwu");
    userInfo.setPassword("123");
    userMapper.add(userInfo);
}

日志信息如下:此时就算把可选字段作为最后一个参数,trim标签会帮我们吧最后一个','删掉,并且在开头和结尾分别添加上'('')'

==> Preparing: insert into userinfo ( username, password ) values ( ?, ? )
==> Parameters: wangwu(String), 123(String)
<== Updates: 1

4.3 <where>标签

where标签自然是为了where子句而设计的,当where子句中只有一个条件并且不确定传不传入的时候,可以使用一个if标签把整个where子句包裹起来。

但是当通过多个参数组合查询的时候就需要用到where标签了,比如用户根据uid+title来查询文章的操作时,又不确定传不传入的情况下:

sql 复制代码
select * from articleinfo where title = 'MyBatis入门' and uid = 1;
java 复制代码
public List<ArticleInfo> selectByCondition(ArticleInfo articleInfo);

where标签的写法十分简单,作用如下:

  • 根据where标签的内容,决定要不要拼接where
  • 去掉最首个参数的and关键字,让sql符合数据库的执行标准

也可以使用<trim prefix = "where" prefixOverrides = "and">标签替换

xml 复制代码
<select id="selectByCondition" resultType="com.chenshu.mybatis_demo.model.ArticleInfo">
    select * from articleinfo
    <where>
        <if test="title != null">
            and title = #{title}
        </if>
        <if test="uid != null">
            and uid = #{uid}
        </if>
    </where>
</select>

使用多种组合查询进行测试,首先要先把实体类中所有int类型的属性的类型改为Integer,否则在不传入参数的时候默认为0,而不是null

java 复制代码
@Data
public class ArticleInfo {
    private Integer id;
    private String title;
    private String content;
    private Timestamp createtime;
    private Timestamp updatetime;
    private Integer uid;
    private Integer rcount;
    private Integer state;
    private String username;
}

接下来我将传入不同的参数进行测试。


1. 什么参数也不传:

java 复制代码
@Test
void selectByCondition() {
    ArticleInfo articleInfo = new ArticleInfo();
    List<ArticleInfo> list = articleMapper.selectByCondition(articleInfo);
    for (ArticleInfo article : list) {
        System.out.println(article);
    }
}

打印SQL日志如下:

==>  Preparing: select * from articleinfo
==> Parameters: 
<==    Columns: id, title, content, createtime, updatetime, uid, rcount, state
<==        Row: 1, Spring Boot入门, <<BLOB>>, 2024-04-15 01:21:43, 2024-04-15 01:21:43, 1, 1, 1
<==        Row: 2, MyBatis入门, <<BLOB>>, 2024-04-15 01:21:45, 2024-04-15 01:21:45, 1, 1, 1
<==        Row: 3, MyBatis的插入操作, <<BLOB>>, 2024-04-16 01:07:49, 2024-04-16 01:07:49, 1, 1, 1
<==        Row: 4, 修改标题, <<BLOB>>, 2024-04-16 01:29:37, 2024-04-16 01:29:37, 1, 1, 1
<==        Row: 6, MyBatis添加并返回自增id, <<BLOB>>, 2024-04-16 01:53:53, 2024-04-16 01:53:53, 1, 1, 1
<==      Total: 5

2. 只传uid:

java 复制代码
@Test
void selectByCondition() {
    ArticleInfo articleInfo = new ArticleInfo();
    articleInfo.setUid(1);
    List<ArticleInfo> list = articleMapper.selectByCondition(articleInfo);
    for (ArticleInfo article : list) {
        System.out.println(article);
    }
}

打印SQL日志如下:

==>  Preparing: select * from articleinfo WHERE uid = ?
==> Parameters: 1(Integer)
<==    Columns: id, title, content, createtime, updatetime, uid, rcount, state
<==        Row: 1, Spring Boot入门, <<BLOB>>, 2024-04-15 01:21:43, 2024-04-15 01:21:43, 1, 1, 1
<==        Row: 2, MyBatis入门, <<BLOB>>, 2024-04-15 01:21:45, 2024-04-15 01:21:45, 1, 1, 1
<==        Row: 3, MyBatis的插入操作, <<BLOB>>, 2024-04-16 01:07:49, 2024-04-16 01:07:49, 1, 1, 1
<==        Row: 4, 修改标题, <<BLOB>>, 2024-04-16 01:29:37, 2024-04-16 01:29:37, 1, 1, 1
<==        Row: 6, MyBatis添加并返回自增id, <<BLOB>>, 2024-04-16 01:53:53, 2024-04-16 01:53:53, 1, 1, 1
<==      Total: 5

3. 传多个参数:

java 复制代码
@Test
void selectByCondition() {
    ArticleInfo articleInfo = new ArticleInfo();
    articleInfo.setUid(1);
    articleInfo.setTitle("MyBatis入门");
    List<ArticleInfo> list = articleMapper.selectByCondition(articleInfo);
    for (ArticleInfo article : list) {
        System.out.println(article);
    }
}

打印SQL日志如下:

==>  Preparing: select * from articleinfo WHERE title = ? and uid = ?
==> Parameters: MyBatis入门(String), 1(Integer)
<==    Columns: id, title, content, createtime, updatetime, uid, rcount, state
<==        Row: 2, MyBatis入门, <<BLOB>>, 2024-04-15 01:21:45, 2024-04-15 01:21:45, 1, 1, 1
<==      Total: 1

4.4 <set>标签

其实set标签与update标签很像,作用是:

  • 根据set标签里的内容决定要不要拼接"set"
  • 去掉最后一个参数的',',让sql符合数据库的执行标准

可以使用<trim prefix="set" suffixOverrides=",">替换。

具体使用:

java 复制代码
public void updateUserInfo(UserInfo userInfo);
xml 复制代码
<update id="updateUserInfo">
    update userinfo
        <set>
            <if test="name != null">
                username=#{name},
            </if>
            <if test="password != null">
                password=#{password},
            </if>
            <if test="photo != null">
                photo=#{photo},
            </if>
            <if test="createtime != null">
                createtime = #{createtime},
            </if>
            <if test="updatetime != null">
                updatetime = #{updatetime},
            </if>
        </set>
    where id = #{id}
</update>

单元测试代码如下,修改字段为1的用户的usernamepassword以及photo字段:

java 复制代码
@Test
void updateUserInfo() {
    UserInfo userInfo = new UserInfo();
    userInfo.setId(1);
    userInfo.setName("zhang");
    userInfo.setPassword("12345");
    userInfo.setPhoto("doge.png");
    userMapper.updateUserInfo(userInfo);
}

修改前表的数据为以下记录:

运行代码后,日志打印信息如下:

==>  Preparing: update userinfo SET username=?, password=?, photo=? where id = ?
==> Parameters: zhang(String), 12345(String), doge.png(String), 1(Integer)
<==    Updates: 1

成功修改了下面三个字段:

4.5 <foreach>标签

当后端传入参数为集合(如List,Set,MapArray)并需要进行遍历 时可以使用该标签,foreach标签有以下属性:

  • collection:绑定方法中用@Param定义的别名
  • item:用于给遍历时的每一个对象取一个名字
  • open:语句块开头的字符串
  • close:语句块结束的字符串
  • separator:每次遍历之间间隔的字符串

示例:根据多个文章id来删除文章数据

原SQL语句:

sql 复制代码
delete from articleinfo where id in(3,4,5)

具体实现:

java 复制代码
public int deleteByIds(@Param("ids") List<Integer> ids);

注意事项:foreach标签中的collection中的值对应接口方法中定义的别名

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

此时表数据如下,我想要删除id字段为7,8,9文章:

单元测试:

java 复制代码
@Test
void deleteByIds() {
    List<Integer> list = new ArrayList<>();
    list.add(7);
    list.add(8);
    list.add(9);
    int ret = articleMapper.deleteByIds(list);
    System.out.println("删除了:" + ret);
}

运行后打印日志信息如下:

==>  Preparing: delete from articleinfo where id in ( ? , ? , ? )
==> Parameters: 7(Integer), 8(Integer), 9(Integer)
<==    Updates: 3

成功删除了三条记录:

相关推荐
你知道“铁甲小宝”吗丶4 分钟前
【第33章】Spring Cloud之SkyWalking服务链路追踪
java·spring boot·spring·spring cloud·skywalking
coffee_baby16 分钟前
化繁为简:中介者模式如何管理复杂对象交互
java·spring boot·microsoft·交互·中介者模式
ღ᭄ꦿ࿐Never say never꧂20 分钟前
微服务架构中的负载均衡与服务注册中心(Nacos)
java·spring boot·后端·spring cloud·微服务·架构·负载均衡
所待.38321 分钟前
小小扑克牌算法
java·算法
.生产的驴29 分钟前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
.生产的驴29 分钟前
SpringBoot 消息队列RabbitMQ在代码中声明 交换机 与 队列使用注解创建
java·spring boot·分布式·servlet·kafka·rabbitmq·java-rabbitmq
idealzouhu42 分钟前
Java 并发编程 —— AQS 抽象队列同步器
java·开发语言
听封1 小时前
Thymeleaf 的创建
java·spring boot·spring·maven
写bug写bug1 小时前
6 种服务限流的实现方式
java·后端·微服务