文章目录
1. 前言
MyBatis是一个基于JDBC的半 ORM 持久化框架, 能通过 注解 或者 XML(主流方式) 和 映射原生类型, 接口和 java 的POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
通俗来说,MyBatis 能够通过简单(指不繁琐)的操作实现对数据库的操作
ORM : Object Relational Mapping, 也就是对象关系映射框架, 它指的是够将数据库中的关系模型和 Java 对象建立起一种对应关系。例如数据库中有 Article 这张表,那么一个 Java 对象对应的就是 Article 这张表中的一行数据
而完成一个 MyBatis 的操作只需要以下几个步骤:
- 创建项目
- 导入 MyBatis 的框架支持,而如果我们要连接的是 MySQL,那么还需要一个 MySQL Driver 的支持(本文重在 MySQL)
- 配置想要连接的数据库以及一些其他的配置
- 完成 XML 文件和 Java 接口的编写(一个 Java 接口对应一个 XML 文件)
- 在 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 文件中又需要获取到对象的属性。所以这时候我们可以使用两种解决方法来获取,如果参数是对象,那么
- 使用 @Param 进行对对象重命名(名字不变也行,但是注释要有),然后在 xml 中使用 #{对象.成员} 的方式获取
- 不使用 @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 和 依赖注入。测试结果如下:符合预期
这样查询操作就完成了,而插入操作是没有变化的,跟原来一样插入即可,全过程图片如下
其他操作基本上都差不多,简单做个总结:
- 首先都是在接口中声明这个方法,并且自行选择要不要添加 @Param 注解。
- 在 xml 文件中添加 <mapper> 标签,并指定相应的接口
- 如果一部分数据库字段名和Java属性名不一样,那么可以针对这一部分进行重映射,需要使用 <resultMap>,并指定对应的 Java 类
- 根据 sql 的操作类型来确定使用哪个标签(insert…),如果没有重映射,那么查询的时候要指定 resultType,如果重映射了,那么 resultType 就要换成 resultMap ,resultMap 填写对应的标识
11. 排序查询
排序查询:如果有这样一个场景:在文章表中,需要我们使用一个方法,就能完成对 访问量 的 升序或者降序查询
如果直接使用 sql 语句,那么是:select * from article order by visitcount asc(或者 desc);
那么想实现这个功能,有聪明的小伙伴可能就想到了:可以进行传参,参数只能是asc 或者 desc
,然后根据这个参数在 xml 中使用 order by visitcount #{参数}
,这个方法听起来挺对的,实际上却行不通,我们来试试
(我们先在表中插入几个元素,方便演示)
然后按照上面的步骤,全过程步骤如下:
但是为啥会报错嘞?
- 首先了解一下 #{} :它的工作原理是预编译处理,MyBatis 在处理 #{} 的时候,会先将其中的变量替换成 ? 号,然后使用 PreparedStatement 中 set 方法进行赋值替换(JDBC中的处理)
- 而 rule 是一个字符串,搭配 #{} 使用的时候,真正执行的 sql 是:
select * from article order by visticount "desc";
但是实际上我们的 desc 是不需要双引号的,我们只需要直接按照原字符串插入就好了,那么有没有直接替换的方法,或者说字符串拼接的方法?有!那就是⭐ ${}
${} 就是直接替换,它的工作原理是字符串拼接。
所以我们只需要稍作修改,在上文基础上将 #{} 改成 ${} 就行:如下
就可以完成自定义升降序了
但是 ${} 有一个致命的缺陷,可能发生 SQL 注入的安全问题,篇幅有限,这里不展开了,CSDN很多佬们都有写🌹
11.1 #{} 和 ${} 的区别
这里涉及到一个常见面试题,总结一下:
- #{} 采用的是预处理方法,比较安全
- ${} 采用的是直接替换
- ${} 含有 SQL 注入的安全隐患
- 但是 ${} 可以实现升序或者降序排序
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>
- 首先这些 select 出来的字段有:article 的全部字段,user 的 username 字段。
- 但是我填写的 resultType 却使用 Article 来接受,但是 Article 实际上却没有 username 这个属性
- ⭐所以我们可以直接在 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 表中,如果 createtime
和 updatetime
不填,那么这两个属性会默认是当前时间,但是如果填的是 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 中的 valueopen
:语句块开头的字符串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>
拼接后的结果
数据库执行情况:情况符合预期
这就是本文所有内容了,如果有错误欢迎大家指出~