MyBatis 万字长文:从入门到动态SQL超详细

1. 前言

MyBatis是一个基于JDBC的半 ORM 持久化框架, 能通过 注解 或者 XML(主流方式) 和 映射原生类型, 接口和 java 的POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

通俗来说,MyBatis 能够通过简单(指不繁琐)的操作实现对数据库的操作

ORM : Object Relational Mapping, 也就是对象关系映射框架, 它指的是够将数据库中的关系模型和 Java 对象建立起一种对应关系。例如数据库中有 Article 这张表,那么一个 Java 对象对应的就是 Article 这张表中的一行数据


而完成一个 MyBatis 的操作只需要以下几个步骤:

  1. 创建项目
  2. 导入 MyBatis 的框架支持,而如果我们要连接的是 MySQL,那么还需要一个 MySQL Driver 的支持(本文重在 MySQL)
  3. 配置想要连接的数据库以及一些其他的配置
  4. 完成 XML 文件和 Java 接口的编写(一个 Java 接口对应一个 XML 文件)
  5. 在 XML 文件中编写 SQL 语句并测试

2. 创建项目

首先是项目的创建,打开 IDEA 新建项目,然后按照如下步骤操作
(如果无法创建 Spring Boot 项目则可以在 IDEA 安装 Spring Boot Helper 插件)
在这里插入图片描述
然后再按如下操作, (如果这里没有添加依赖, 后面仍然可以添加)

在这里插入图片描述到此为止, 一个项目就算创建完了, 然后在刚开始目录结构可能有点复杂, 我们可以删除 4 个没用的文件, 如下

在这里插入图片描述

3. 添加框架支持

如果创建项目之前已经添加了依赖,这一步就可以直接忽略。但是建议大家装一个 EditStarters 插件,后续导入依赖很方便。
在项目自带的 pom.xml 文件中右键 → generate → EditStarters

在这里插入图片描述

然后勾选以下这几项,然后等待下载即可

在这里插入图片描述

4. 建库

在练习 MyBatis 之前,需要本地建个数据库和建表

想练习的也可以复制下面这段一样的建库代码,这里创建了文章表和作者表(复制粘贴到 MySQL 即可)

-- 创建数据库
drop database if exists practice;
create database practice DEFAULT CHARACTER SET utf8mb4;

-- 使用数据数据
use practice;

-- 创建用户表
drop table if exists  user;
create table user(
    id int primary key auto_increment,
    username varchar(100) not null,
    password varchar(32) not null,
    createtime datetime default now(),
    updatetime datetime default now()
) default charset 'utf8mb4';

-- 创建文章表
drop table if exists  article;
create table article(
    id int primary key auto_increment,
    title varchar(100) not null,
    content text not null,
    createtime datetime default now(),
    updatetime datetime default now(),
    userid int not null, 			      -- 用户 id
    visitcount int not null default 1     -- 该文章访问次数
)default charset 'utf8mb4';


-- 随便添加一个作者数据
insert into user(`id`, `username`, `password`, `createtime`, `updatetime`) values
(1, 'pig', 'pig', '2022-12-14 00:00:00', '2022-12-14 00:00:00');

-- 随便添加一个文章数据
insert into article(title,content,userid)
    values('MyBatis博客','MyBatis增删查改',1);

表结构如下

在这里插入图片描述

5. 配置数据库连接信息和 XML 文件路径

首先在 resources 目录底下创建一个 applictaion.yml 文件,然后在全局配置文件 application.yml 中(也是可以是 properties 文件,但是代码略有差异,效果一样,本文演示的是 yml 文件), 加入下面这一段代码。

⭐在 MyBatis 中,需要去定义 SQL 映射语句,一个MyBatis 的 xml 文件保存的是操作数据库的 sql 语句,而一个 xml 文件会实现一个Java接口中相应的操作数据库的方法,但是我们还需要让 MyBatis 知道这些 xml 文件在哪里,所以这里还需要配置 xml 的文件路径。

spring:
  datasource:
    # 配置本地连接的数据库(此处我的数据库名字是 practice)
    url: jdbc:mysql://127.0.0.1/practice?characterEncoding=utf8
    username: root
    password: 123456  # 自己数据库的密码
    driver-class-name: com.mysql.cj.jdbc.Driver # 固定写法
mybatis:
  # 配置 xml 文件路径,此处表示配置在 当前目录的 mapper 包中
  mapper-locations: classpath:mapper/**Mapper.xml 的 xml 文件都是

上面代码中的 mapper-locations 的作用就是配置 xml 的文件路径,表示:当前目录下有个 mapper 包,并且包中以 xxMapper.xml 形式命名的文件都是和接口对应的 xml 文件

5.1 创建 Java 类

前面说到 MyBatis 是一个半 ORM 框架,需要将数据库中的数据模型和 Java 对象对应起来,所以还需要创建和数据库中的表对应的 Java 类。

而我们数据库中有 user 和 article 两张表,表中的一条数据就会对应一个 Java 对象,所以,分别创建 Article 类和 User 类(路径不限)。这里我们先拿 Article 举例子,这个表不涉及「字段名和属性名不相等」的问题。后面再拿 User 举重映射的例子

@Data  // 这个注解就是给该类加上 toString, get, set 等方法
public class Article {
    private int id;
    private String title;
    private String content;
    private Date createtime;
    private Date updatetime;
    private int userid;
    private int visitcount;
}

(该 Article 类属性名和数据库字段名相等)

5.2 Java 接口

在 MyBatis 中,一个 xml 文件和一个 接口是对应的,对于一个类,就会有一个接口和实现相关操作的 xml 文件

⭐但是这个接口不只是普通的接口,还需要加上 @Mapper 注解来修饰,这个注解会对这个接口生成一个实现类,会将其类实例化成 Bean 对象存入 Spring 中进行管理。

我们开始编写这个 接口 (接口命名无要求)

@Mapper
public interface ArticleMapper {
}

5.3 XML 文件

然后就是创建 xml 文件,由于在前面我们配置了 xml 文件的路径,如下

在这里插入图片描述
因此我们的 xml 不仅需要放在同一级目录中的 mapper 包中,还需要以 __Mapper.xml 的形式命名 xml 文件,这里我们就取名 ArticleMapper.xml

在这里插入图片描述

⭐然后在 MyBatis 中的 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">

在 xml 文件中,首先我们需要指定当前文件要和哪个接口进行对应,然后使用标签和 sql 语句去“实现”该接口中的哪个方法。

接着在下面然后添加 <mapper> 标签,namespace 填的是当前 xml 对应的接口的完整路径,这里填的是 ArticleMapper 的路径,然后后续在该 <mapper> 标签中编写代码即可

<mapper namespace="com.example.blogtest.mapper.ArticleMapper"> 
<? 后续内容都是在这个 Mapper 标签中填写 ?>
</mapper>

这个是我的目录结构

在这里插入图片描述

至此为止,我们就可以正式填写我们操作数据库的代码了。

⭐而关于后续的增删查改,都是先在接口中声明一个方法,然后在对应的 xml 文件中对这个方法编写相应的标签和 sql 语句
⭐而 sql 语句是用来用来操作数据库的,这里的 sql 语句和直接在 MySQL 中输入 sql 语句有些相似。


6. 查询

6.1 不带参数的查询

任务1:查询文章表中的所有文章

(1)首先在接口中声明这个方法,由于查询所有文章,所以我们需要返回 List

@Mapper
public interface ArticleMapper {
    List<Article> selectAll(); // 声明方法
}

(2)然后在该接口对应的 xml 文件中编写 sql 语句,查询语句需要用到 <select> 标签,而想要对应上接口中的某个方法,则还需要填写两个属性:

  • id :填写接口中对应的方法,这个例子中填的就是 “selectAll”
  • resultType:返回的对象的类型的全路径(注意:这里填的不是容器的类型)

(3)sql 语句的编写
⭐在该 xml 文件中编写的 sql 是和写在 MySQL 中的 sql 语句差不多的
如果是直接在 MySQL 中查询,那么 sql 语句就是select * from article,而这里没有参数,所以也就不用做修改。

<mapper namespace="com.example.blogtest.mapper.ArticleMapper">  <!-- 这里填的是接口路径 -->
    <select id="selectAll" resultType="com.example.blogtest.model.Article">  <!-- 这里填的是对象路径 -->
        select * from article
    </select>
</mapper>

这样就完成了查询操作。然后我们需要检验一下是否符合预期(xml 中的 sql 语句可以不加分号)

6.2 单元测试

为了测试我们的代码有没有问题,我们可以使用单元测试来验证,其依赖 SpringBoot 项目中默认带有,所以还省事,只需要按以下步骤操作即可。(点击 OK 之后如果出现 ERROR,继续往下 Next 即可)

在这里插入图片描述增加单元测试之后,还需要在该测试类的头上加上 @SpringBootTest 注解,然后再补充点代码,将得到的数据打印出来。如下代码

@SpringBootTest // 这步很重要, 容易忘
class ArticleMapperTest {
    @Autowired
    ArticleMapper mapper; // 注入依赖
    @Test
    void selectAll() {
        List<Article> list = mapper.selectAll(); // 使用容器接收 selectAll 放回的数据
        for (Article a : list) {    // 打印
            System.out.println(a);
        }
    }
}

上述代码中还需要注意:原本 ArticleMapper 是一个接口,但是加上了 @Mapper 之后,会将经过一定处理实例化成Bean并存入 Spring 进行管理,所以此处进行依赖注入,才能测试它的方法。

最后点击下图的运行即可:
在这里插入图片描述

测试结果如下

在这里插入图片描述这就算完成 MyBatis 中最简单的查询操作了。接下来结合例子讲讲第二个带参数的查询

6.3 带参数的查询

任务2:查询指定 id 的文章

(1)第一步还是在接口中声明这个方法,但是由于 id 是我们指定的变量,所以我们需要进行传参,具体到接口的方法中,就是需要使用 @Param(“”) 修饰变量,作用就是给修饰的参数命名,能够让 MyBatis 正确识别到这个参数。如下

@Mapper
public interface ArticleMapper {
	// newId 是给 id 起的新名字,如果不加上这个注解,那么默认名字就是 id
    List<Article> selectById(@Param("newId")Integer id);
}

如上,使用 @Param 注解为 id 起了个 “newId” 的名字,而这个名字就是使用在 xml 文件中。如果不加该注解,那么表示名字没有修改,具体到 MyBatis 中就是直接使用原名。

(2) 然后在该接口对应的 xml 文件中编写该方法的代码,而 id 是一个变量,在MyBatis 中想要使用这个变量参数, 就需要使用 #{} , 括号中间放的是参数的名字(注解起的名,没加注解就是原名)

如果是在 MySQL 中查询,那么 sql 语句是:select * from article where id = ?,而在 MyBatis 中,这里的 ” ? “ 就使用 #{} 并且括号中添加参数的名字来实现,如下。

	<select id="selectById" resultType="com.example.blogtest.model.Article">
        select * from article where id = #{newId}
    </select>

(3) 单元测试:还是和上面一样生成测试方法,然后将得到的数据打印出来

    @Test
    void selectById() {
        List<Article> list = mapper.selectById(1); // 查询id 为 1 的文章
        for (Article a : list) {
            System.out.println(a);
        }
    }

然后当我们传参 id 为 1 的时候,运行后查询结果如下,符合预期

在这里插入图片描述
而关于排序查询,联合查询…我们下文会讲到

7. 修改

学会了带参数的查询之后,修改操作也不难了。

任务:将 article 表中指定 id 的文章内容修改成指定 content

(1)在接口中声明这个方法,此时有两个参数:id 和 content,代码如下(该例子中不加 @Param 也行)。并且返回值为 int,表示受影响的行数

    // 将 id = ? 的文章的 content 改成 ?
    int updateContentById(@Param("id") int id, @Param("content") String content);

(2)在 xml 中添加 <update> 标签,并且在该标签中添加参数 id ,并为其设定方法名。然后编写 sql 语句。

如果在 MySQL 中操作,sql 语句:update article set content = ? where id = ?
所以在 MyBatis 中,将 " ? " 替换成 #{变量名} 即可,如下:

    <update id="updateContentById">
        update article set content = #{content} where id = #{id}
    </update>

(3) 生成测试方法,原表内容如下,假设我们将 id 为 1 的文章内容修改为 “准备冻手”,那么代码如下
在这里插入图片描述

	@Test
    void updateContentById() {
        mapper.updateContentById(1, "准备冻手");
        System.out.println("修改成功");
    }

然后执行单元测试,article 表如下,符合预期,修改操作完成。

在这里插入图片描述

8. 增加

8.1 将对象插入表中

任务:添加一篇文章

(1)第一步,在接口中声明方法,并且这个方法是带参数的,所以我们也给它命个名
(在 sql 中,insert 语句返回的是受影响的行数,所以这里返回值自然是 int 类型)

	// 新增一篇文章
    int add(@Param("article") Article article);

(2)在 xml 文件中编写代码。这里要用到的标签是 <insert> ,标签里面依然有个参数 id,填写其所实现的方法名,具体到此就是 “add” 。

但是这里和上面带参数的查询有点不一样了, 这里的参数是一个对象,那么这时候需要怎么做呢?
假设想要插入一条「id = 2, title=“科学养猪”, content=“发家致富”, userid=1」这样的文章,那么 sql 语句是

insert into article (id, title, content, userid) values (2, "科学养猪", "发家致富", 1);

⭐由于我们的参数是一个对象,而 xml 文件中又需要获取到对象的属性。所以这时候我们可以使用两种解决方法来获取,如果参数是对象,那么

  1. 使用 @Param 进行对对象重命名(名字不变也行,但是注释要有),然后在 xml 中使用 #{对象.成员} 的方式获取
  2. 不使用 @Param, 也就是对于 xml 来说,名字就是原参数名,然后在 xml 中使用 #{成员} 的方式来获取

建议大家记住其中一种进行使用就好,否则容易乱,笔者记得是第一种,本文也使用第一种举例,虽然啰嗦但是能够重命名。

因此在 xml 中的代码如下:

    <insert id="add">
        insert into article (id, title, content, userid) 
        values (#{article.id}, #{article.title}, #{article.content}, #{article.userid})
    </insert>

随后生成单元测试,然后构造上述对象,并调用该接口中的 add 方法

    @Test
    void add() {
        Article article = new Article();
        article.setId(2);
        article.setTitle("科学养猪");
        article.setContent("发家致富");
        article.setUserid(1);
        mapper.add(article); // 调用接口中的 add 方法,将这个对象传入
        System.out.println("添加成功");
    }

然后运行这个方法,查看数据库中的表,如下,文章新增成功,也符合我们的预期

在这里插入图片描述

这样就玩完成了插入一个对象了

8.2 获取自增主键

因为增加操作的时候只会返回受影响的行数,而如何我们想要获取到增加的对象的自增 id ,需要怎么操作?
(1)这个操作和普通的增加操作区别不大,还是先定义方法

    // 在添加新元素的时候,获取新元素的自增 id
    int addAndGetId(@Param("article") Article article);

(2)然后在 xml 中,我们需要在 <insert> 标签中加入两个属性:useGeneratedKeys 和 keyProperty

  • useGeneratedKeys:默认是 false,如果为 true,那么 MyBatis 会取出数据库内部生成的主键
  • keyProperty:需要填写能够唯一识别对象的属性(是实体类的属性,不是数据库的字段),它会将新增行的主键赋值给插入的对象相应的属性。有点绕,举个例子就知道了👇

代码如下,useGeneratedKeys 设置为 true 表示「开启自动生成」,keyProperty 设为 id,表示会将新增行的主键赋值给「插入的对象的 id 属性中」,具体什么意思往下看:

    <insert id="addAndGetId" useGeneratedKeys="true" keyProperty="id">
        insert into article (id, title, content, userid) 
        values (#{id}, #{title}, #{content}, #{userid})
    </insert>

(3)生成单元测试,代码如下(这里自增主键的进一步解释在代码的注释中)

    @Test
    void addAndGetId() {
        // 这个是新插入的对象
        Article article = new Article();
        article.setId(3);
        article.setTitle("获取自增主键");
        // 然后 xml 文件中 keyProperty = 'id',表示会将新增行的主键 id 赋值给这个对象的 id 属性!
        article.setContent("通过 useGeneratedKey 和 keyProperty 标签来设置");
        article.setUserid(1);
        mapper.add(article); // 调用接口中的 add 方法,将这个对象传入
        // 由于新行的自增主键赋值给了这个对象的 id 属性,所以我们直接通过 article.getId() 方法就可以获取 自增主键
        System.out.println(article.getId());
    }

然后执行,数据库查询结果和终端打印结果如下:也符合预期
在这里插入图片描述

9. 删除

看到这里,插入操作就没什么难度了,这里简单讲讲就过了

任务: 删除指定 id 的文章

(1)在接口中声明这个方法

int delete(@Param("id") Integer id);

(2)在 xml 文件中添加 <delete> 标签,并指定标签 id 的值

    <delete id="delete">
        delete from article where id = #{id}
    </delete>

(3)生成单元测试,将 id = 3 的文章删除

    @Test
    void delete() {
        mapper.delete(3);
    }

执行即可,然后查询数据库可以看出,id 为 3 的文章被正确删除了,也符合我们的预期
在这里插入图片描述

至此,MyBatis 中,当 Java 类的属性名都和数据库字段名一致的时候,增删查改就没有什么问题了。
接下来我们再来看看如果 Java 中类属性和数据库类属性不一样,需要怎么处理

10. 数据库字段和类属性名字不一致的情况

上面创建了文章表和作者表,现在拿作者表举例子,如下是文章表的结构

在这里插入图片描述

然后就需要要表中创建这个 User 类,但是我这时候觉得 username,createtime,updatetime 没有使用小驼峰,不好看,所以采用小驼峰的方式创建这个类。(这个类只是一个普通的类,不用加 mapper 注解,但是可以加上 @Data 注解,需要导入lombok依赖,上文已经有导入,能够自动为该类添加 set,get,toString 等方法)

@Data
public class User {
    int id;
    String userName; // 使用驼峰命名
    String password;
    Date createTime; // 使用 Date 类型即可
    Date updateTime;
}

然后现在需要对 作者表 进行操作了, 所以现在也需要重新创建一个接口,来声明相应操作数据库的方法。如下,别忘了加上 @Mapper 注解,再重复一下它的作用:它会将为该接口生成实现类,并为其实例化成 Bean 对象存入 Spring 中。

@Mapper
public interface UserMapper { // 命名无要求
}

10.1 查询

任务:根据 id 查询作者

(1)第一步还是声明,其实重映射的情况和上面的情况是很相似的。

    // 根据 id 查询文章
    List<User> selectById(@Param("id") Integer id);  // 返回值是 List 类型,使用 List 接收

(2)编写 xml 文件,这个就需要动点手脚了。
还是一样,需要在指定的路径下创建 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,填的是对应接口的全路径。

<mapper namespace="com.example.blogtest.mapper.UserMapper">
    <? 后续内容都是都是在这个 mapper 标签中写 ?>
</mapper>

10.2 resultMap

(3)⭐指定完接口之后,就有点不一样了,我们需要对「数据库中的字段名和Java类中的属性」进行重映射,将数据库字段和Java类属性名字不一样的属性重新对应起来。这里需要用到 <resultMap> 标签,在头标签中需要填写两个参数,id 和 type

  • id:resultMap 标签的标识,就是给它起个名。
  • type:需要进行重映射的 Java 类路径,这里我们需要对 User 类进行重映射,所以路径就是 User 全路径

如下

	<resultMap id="map" type="com.example.blogtest.model.User">
        <? 这里填写 数据库字段 和 Java 类的重映射 ?>
    </resultMap>

然后在该 resultMap 中填写要重映射的内容,其中主要有两个标签 <id> 和 <result>(属性有点多,但是不难,举个例子就明白了)

  • id ,用于设置主键字段与领域模型属性的映射关系
  • result ,用于设置普通字段与领域模型属性的映射关系

其中这两个标签需要填写:① column:数据库中的字段 ② property:Java 类中的属性

至此就是 <resultMap> 的使用方式了,我们来操作一下(搭配注释食用):

<mapper namespace="com.example.blogtest.mapper.UserMapper">
	<? resultMap 需要指定 id, 和重映射的类路径 ?>
    <resultMap id="map" type="com.example.blogtest.model.User">
    	<? id 标签用于主键的重映射,result 用于普通属性的重映射 ?>
    	<? column 填的是数据库字段,property 填的是 Java 类属性?>
        <id column="id" property="id"></id>   <? 主键 ?>
        <? 将数据库中 username 和 Java 类中的 userName 重新对应上 ?>
        <result column="username" property="userName"></result>  
        <result column="createtime" property="createTime"></result>
        <result column="updatetime" property="updateTime"></result>
    </resultMap>
    <? 接下来的增删查改在 resultMap 标签的后面写 ?>
</mapper>

⭐而没有进行处理的字段会按照默认的规则来对应,也就是默认数据库字段和类属性名字一致的方式来对应。

好了,然后在 xml 文件中我们继续处理「根据 id 查询文章」
那么现在的 select 标签需要怎么写?首先 id 是肯定要写的,填接口中对应的方法名。但是这里不是使用 resultType 了,而是 resultMap,⭐并且 resultMap 填写相应的 resultMap 标识,也就是上面进行重映射的 resultMap 名字。

具体点,在这里就是:

    <select id="selectById" resultMap="map"> <? 填的是 resultMap 不是 resultType ?>
        select * from user where id = #{id}
    </select>

(4)生成单元测试,将结果打印出来,别忘了添加 @SpringBootTest 和 依赖注入。测试结果如下:符合预期

在这里插入图片描述
这样查询操作就完成了,而插入操作是没有变化的,跟原来一样插入即可,全过程图片如下

在这里插入图片描述其他操作基本上都差不多,简单做个总结:

  1. 首先都是在接口中声明这个方法,并且自行选择要不要添加 @Param 注解。
  2. 在 xml 文件中添加 <mapper> 标签,并指定相应的接口
  3. 如果一部分数据库字段名和Java属性名不一样,那么可以针对这一部分进行重映射,需要使用 <resultMap>,并指定对应的 Java 类
  4. 根据 sql 的操作类型来确定使用哪个标签(insert…),如果没有重映射,那么查询的时候要指定 resultType,如果重映射了,那么 resultType 就要换成 resultMap ,resultMap 填写对应的标识

11. 排序查询

排序查询:如果有这样一个场景:在文章表中,需要我们使用一个方法,就能完成对 访问量 的 升序或者降序查询
如果直接使用 sql 语句,那么是:select * from article order by visitcount asc(或者 desc);那么想实现这个功能,有聪明的小伙伴可能就想到了:可以进行传参,参数只能是asc 或者 desc,然后根据这个参数在 xml 中使用 order by visitcount #{参数},这个方法听起来挺对的,实际上却行不通,我们来试试
(我们先在表中插入几个元素,方便演示)
在这里插入图片描述然后按照上面的步骤,全过程步骤如下:

在这里插入图片描述
但是为啥会报错嘞?

  1. 首先了解一下 #{} :它的工作原理是预编译处理,MyBatis 在处理 #{} 的时候,会先将其中的变量替换成 ? 号,然后使用 PreparedStatement 中 set 方法进行赋值替换(JDBC中的处理)
  2. 而 rule 是一个字符串,搭配 #{} 使用的时候,真正执行的 sql 是:select * from article order by visticount "desc";

但是实际上我们的 desc 是不需要双引号的,我们只需要直接按照原字符串插入就好了,那么有没有直接替换的方法,或者说字符串拼接的方法?有!那就是⭐ ${}

${} 就是直接替换,它的工作原理是字符串拼接。
所以我们只需要稍作修改,在上文基础上将 #{} 改成 ${} 就行:如下
在这里插入图片描述就可以完成自定义升降序了
但是 ${} 有一个致命的缺陷,可能发生 SQL 注入的安全问题,篇幅有限,这里不展开了,CSDN很多佬们都有写🌹

11.1 #{} 和 ${} 的区别

这里涉及到一个常见面试题,总结一下:

  1. #{} 采用的是预处理方法,比较安全
  2. ${} 采用的是直接替换
  3. ${} 含有 SQL 注入的安全隐患
  4. 但是 ${} 可以实现升序或者降序排序

12. 模糊匹配

任务:查找标题中带有某关键词的文章

这里就需要用到 MySQL 自带的字符串拼接函数 concat 了。使用方式👇,能够将多个字符串拼接起来。

CONCAT(string1,string2, … )

举个例子:
(1)声明方法

    // 查找标题中带有 某关键词 的文章
    List<Article> selectByLike(@Param("key") String key);

(2)xml 文件中编写代码

    <select id="selectByLike" resultType="com.example.blogtest.model.Article">
        select * from article where title like concat("%", #{key}, "%")
    </select>

然后生成单元测试,查询一下标题中带有 “手机” 的文章

    @Test
    void selectByLike() {
        List<Article> list = mapper.selectByLike("手机");
        for (Article a : list) {
            System.out.println(a);
        }
    }

查询结果如下:符合预期
在这里插入图片描述

13. 联合查询

这里我分享一个比较取巧的方法,操作也比较简单。

任务:结合两个表,查询所有用户的所有文章,并显示作者姓名

(1)声明方法

    // 联合两张表,查询所有作者的所有文章,并显示作者姓名
    List<Article> selectByUserId();

(2)在 xml 中编写代码,我们需要查询出来的字段有:文章表中的所有属性 + 作者的姓名。筛选条件自然就是 article.userid = user.id
首先写出 sql 语句: select user.username, article.* from article join user on user.id = article.userid;;

因此在 xml 中的代码就是:

    <select id="selectByUserId" resultType="com.example.blogtest.model.Article">
        select user.username, article.* from article join user on user.id = article.userid
    </select>
  1. 首先这些 select 出来的字段有:article 的全部字段,user 的 username 字段。
  2. 但是我填写的 resultType 却使用 Article 来接受,但是 Article 实际上却没有 username 这个属性
  3. ⭐所以我们可以直接在 Article 实体类中添加这个 username 属性!(也就是缺少什么属性,就在该类上补充什么属性就行了)这个方法较为取巧,没有用到其他标签

然后我们生成单元测试执行后的结果如下:符合预期

在这里插入图片描述
这样就完成多表查询了,比较取巧,也比较好掌握。
涉及到多表查询也是一样,在指定 resultType 这个类的基础上,多表查询还缺少哪个属性,就添加哪个属性。

14. 动态 SQL

动态 SQL 是 MyBatis 中一个很强大的特性,它能够将 sql 语句进行很方便的拼接
本文主要分享5个标签的使用,在分享之前,可以在配置文件中配置如下代码:
可以在控制台中打印实际上执行的 sql 语句

mybatis:
	configuration:
		log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

14.1 if 标签

在某些场景中,对于表中的某个属性,填的是还是 null 是有很大区别的,例如,在本文中的 user 表中,如果 createtimeupdatetime 不填,那么这两个属性会默认是当前时间,但是如果填的是 null,那就是不一样的结果,可能会对业务逻辑造成一定不便,例如:当不我们不填和填为 null 的两种情况,createtime 实际值如下。(user表在创建时指定了如果 createtime和updatetime 如果不填默认为当前时间)
在这里插入图片描述因此,在 sql 语句中,我们可以通过 <if> 标签来根据某个条件是否成立来决定是否拼接 sql 语句,举个例子就明白了

任务:向 user 表中插入一个新数据,一定要有 id,username,password,但是 createtime 这个属性要取决于用户:如果用户没填(也就是 null),那就默认是当前时间,否则按照用户填的来

(1)在接口中声明这个方法

// 插入(但是 createtime 可能为空)
int insertIfCondition(@Param("id") Integer id, @Param("username") String userName,
                      @Param("password") String password, @Param("createtime")Date createTime);

(2)在 xml 文件中编写 sql 语句
<if> 标签中还需要填写一个属性 test,表示:什么条件成立的情况下,<if>中的语句块参与拼接,test中的条件不成立,就不参与拼接。举个例子就懂了,xml 中编写代码如下:
在这里插入图片描述
(3)然后生成单元测试看一下是否符合预期,首先我们插入一个让 createtime 为空的数据,如下

@Test
void insertIfCondition() {
    mapper.insertIfCondition(5, "测试", "test", null); // createtime 为空
}

然后运行,可以分别从控制台中看到实际的 sql 执行情况以及数据库中查看实际效果👇:
在这里插入图片描述

在这里插入图片描述然后我们再测试一下 createtime 不为空的时候

    @Test
    void insertIfCondition() {
        mapper.insertIfCondition(6, "createtime不为空的test", "test", new Date());
    }

执行后,查看真正执行的 sql 语句以及数据库的查询结果,可以看出,createtime 不为空,所以<if>标签中的语句块拼接正常

在这里插入图片描述

在这里插入图片描述

总结一下:<if>标签能够依赖于 test 属性中的条件,如果条件成立,那么该标签中的语句块参与拼接,否则不参与。⭐并且对于 test 属性用的变量名,使用的是「@Param 注解重命名的变量」,图解如下

在这里插入图片描述

14.2 trim 标签

<trim>标签能够对语句块进行前缀,后缀的添加和删除,其中有 4 个属性

  • prefix:表示标签中的语句块要以 prefix 作为前缀
  • suffix:表示标签中的语句块要以 suffix 作为后缀
  • prefixOverrides:表示整个语句块要删除的前缀
  • suffixOverrides:表示整个语句块要删除的后缀

在这里插入图片描述

文绉绉的,举个例子就明白了

假设我们要向 user 表中插入一个新数据,但是除了 id 每一个值都有可能为 null ,也就是说我们要用 if 语句处理每一个属性。
(需要注意,在 user 表中设定了 id, username, password 都不能为空,这里只是举个这样的场景)

(1)还是一样,先在接口中声明这个方法

// 插入(但是除了id,在上述场景中每一项都可能为空)
int insertTrim(@Param("id") Integer id, @Param("username") String userName,
               @Param("password") String password, @Param("createtime")Date createTime, 
               @Param("updatetime") Date updateTime);

(2)在 xml 文件中编写代码,如果只需要对其余的属性进行单独处理,那么有些小伙伴可以会这么做:
在这里插入图片描述

但是这样就会有一个问题,由于有些变量可能有,可能没有,你也就不知道哪个变量会是括号中的最后一个,所以逗号的问题就会比较尴尬,上述代码可能会出现这种 sql 语句:insert into user (id, username, ) values (idValue, usernameValue, )显然这些多余的逗号在 mysql 是报错的,这时候就可以使用 <trim>标签

代码如下👇,为方便解释,注释会说的比较详细(代码看着多,实际上都是标签,不男)

<insert id="insertTrim">
    insert into user
        <!-- trim 中的 prefix 和 suffix 能分别对语句块增加相应的前缀和后缀 -->
        <!-- 所以这里就直接省略括号了,通过这两个属性添加 -->
        <!-- 我们为每个字段都加上一个逗号,然后使用 suffixOverrides 将作为后缀的最后一个逗号去掉 -->
        <trim prefix="(" suffix=")" suffixOverrides=",">
            id,
            <if test="username != null">
                username,
            </if>
            <if test="password != null">
                password,
            </if>
            <if test="createtime != null">
                createtime,
            </if>
            <if test="updatetime != null">
                updatetime,
            </if>
        </trim>
    values 
        <!-- 这里和上面是一样的,需要注意需要使用 #{变量名} -->
        <trim prefix="(" suffix=")" suffixOverrides=",">
            #{id},
            <if test="username != null">
                #{username},
            </if>
            <if test="password != null">
                #{password},
            </if>
            <if test="createtime != null">
                #{createtime},
            </if>
            <if test="updatetime != null">
                #{updatetime},
            </if>
        </trim>
</insert>

(3)然后生成单元测试

    @Test
    void insertTrim() {
        mapper.insertTrim(7, "trim测试", "test", null, null);
    }

如下,这是实际上的 sql 执行情况,可以看出:由于createtime 和 updatetime为空,所以这两属性就没参与拼接,多余的逗号也被删除了。

在这里插入图片描述数据库查询情况:也符合预期
在这里插入图片描述

总结一下:<trim>能够通过 prefixOverrides 和 prefixOverrides属性去除掉多余的前缀或者后缀,而 prefix 和 suffix属性能够将语句块和相应的前缀和后缀进行拼接

14.3 where 标签

接着是<where>标签,显然,这个是用于「需要用到 where 的情况」

场景:用户想要在 user 表中根据 id 和 username 进行查询,但是这两个筛选条件都可能为空
(显然,如果两个都为空,那么会是查询 user 表中所有记录)

  • <where>标签中如果没有任何语句块,那么 where 关键字就不会存在;相反,如果有语句块,那 where 自然就在
  • 并且 where 中的语句块以 and 或 or 开头,那么会自动删除

(1)在接口中声明方法

    // 根据 id, userName 查询 user 表, 但 id 和 userName 都可能为空
    List<User> selectWhere(@Param("id") Integer id, @Param("username") String userName);

(2)在 xml 文件中编写代码,因为两者都可能不存在,所以使用 where 标签嵌套两个 if 标签实现:

    <select id="selectWhere" resultMap="map"> <? User 类在上文进行了重映射,所以这里要用 resultMap ?>
        select from user
        <where>
            <if test="id != null">
                and id = #{id}
            </if>
            <if test="username != null">
                and username = #{username}
            </if>
        </where>
    </select>

(3)生成单元测试

    @Test
    void selectWhere() {
        List<User> list = mapper.selectWhere(2, "小刘");
        for (User user : list) {  // 查询结果都打印出来
            System.out.println(user);
        }
    }

然后测试一下,当这两个都不为空的时候,执行的 sql 和 查询结果如下:符合预期
在这里插入图片描述
当我们 id = 2,username = null 的时候,测试结果如下:符合预期

在这里插入图片描述
当我们输入两个都为空的时候,测试结果如下:从实际执行的 sql 可以看出两个属性都为空的时候,where 也自然被删除了,这里查询的就是所有数据

在这里插入图片描述
总结一下:这就是<where>标签了,where 语句中的条件都是通过 and 或者 or 连接条件的,由于 where 的特性,我们在所有条件前都加上 and 或者 or 就可以了。

14.4 set 标签

这个标签和 where 标签很相似,where 用于筛选,set 用于update语句中

  • <set>标签会自动将最后的逗号去掉

场景:更新指定 id 的用户,修改用户名和密码,两者都可以为空

(1)在接口中声明方法

    // 修改指定 id 的用户的用户名和密码
    int updateSet(@Param("id") Integer id, @Param("username") String userName, 
                  @Param("password") String password);

(2)在 xml 文件中编写代码

    <update id="updateSet">
        update user
            <set>
                <if test="username != null">
                    username = #{username},
                </if>
                <if test="password != null">
                    password = #{password},
                </if>
            </set>
        where id = #{id};
    </update>

(3)生成单元测试,首先将 id 为 2 的用户,用户名修改为“史强”,密码不做修改
执行的 sql 和数据库查询结果如下👇,<set>标签将最后一个逗号也删除了,符合预期:

在这里插入图片描述在这里插入图片描述总结一下:<set>标签用于 update 语句,用于更新一个或者多个值,并且可以删除最后一个多余的逗号。需要注意的是,当 <set> 标签的使用至少是要更新一个属性的,否则报错

14.5 foreach 标签

接下来是本文介绍的最后一点了:<foreach>标签,在我们需要遍历一个集合的时候,就可以用到,里面包含 5 个属性(但是不复杂,举个例子就明白了)

  • collection:表示迭代集合的名称,可以填写 @Param 注解指定的名字
  • item:该集合在进行迭代时起的别名,如果集合是 Map 集合,这里进行迭代的对象是 key-value 中的 value
  • open:语句块开头的字符串
  • close:语句块结尾的字符串
  • separator:每次遍历之间间隔的字符串

场景:在 user 表中删除 id 在指定集合(参数)中的记录

前言:正常情况下我们想从 user 表中删除若干个 id 的记录,sql 语句可以是:delete from user where id in (....),那么如果我们有了想删除的 id 集合,在 MyBatis 中的实现就要用到<foreach> 标签,它和 Java 中的 foreach 差不多,⭐不同的是它能够将遍历的元素按照一定的规则拼接起来

在这里插入图片描述

(1)在接口中声明这个方法,使用 @Param 起个顺眼的名字

    // 将 user 表中 id 在 idList 中的记录都删除掉
    int deleteForeach(@Param("collection") List<Integer> idList);

(2)在 xml 文件中编写代码

    <delete id="deleteForeach">
        delete from user where id in
            <foreach collection="collection" item="curId" open="(" close=")" separator=",">
                #{curId}
            </foreach>
    </delete>

这段代码是啥意思呢?下面是详细解析👇:

在这里插入图片描述

因此,这段<foreach>标签最终就会得到(id1,id2,id3...)这样的字符串,最终执行这样的 sql 语句
(3)生成单元测试,假设我们传入一个 (4, 5, 6) 的 id 集合:

    @Test
    void deleteForeach() {
        List<Integer> idList = new ArrayList<>();
        idList.add(4);
        idList.add(5);
        idList.add(6);
        mapper.deleteForeach(idList);
    }

执行的 sql 情况如下,in 后面的内容就是 <foreach> 拼接后的结果
在这里插入图片描述数据库执行情况:情况符合预期
在这里插入图片描述

这就是本文所有内容了,如果有错误欢迎大家指出~

15. 全代码

Github

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

答辣喇叭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值