文章目录
- 前言
- 一、初识 MyBatis
- 1.1 什么是 MyBatis
- 1.2 为什么学习 MyBatis
 
- 二、MyBatis 在软件开发框架中的定位
- 三、基于 Spring Boot 创建 MyBatis 项目
- 3.1 添加 MyBatis 框架的支持
- 3.2 配置数据库连接信息和映射文件的保存路径(Mapper XML)
 
- 四、MyBatis 项目结构的创建与使用
- 4.1 数据库和表的准备
- 4.2 根据数据库表创建实体类
- 4.3 创建 Mapper 接口和 XML 映射文件
- 4.4 创建服务层 Service 和 控制层 Controller
 
- 五、通过 MyBatis 实现增、删、改操作
- 5.1 增加用户
- 5.2 修改用户
- 5.3 删除用户
 
- 六、通过 MyBatis 实现查询操作
- 6.1 单表查询
- 6.1.1 通过用户 ID 查询
- 6.1.2 参数占位符 #{} 和 ${}
- 6.1.3 SQL 注入问题
- 6.1.3 like 查询
- 6.1.4 使用 resultMap 解决实体类参数与数据库表字段不匹配问题
 
- 6.2 多表查询
- 6.2.1 VO类的创建
- 6.2.2 创建 Mapper 接口和 XML 映射文件
- 6.2.3 查询文章详情
 
 
- 七、MyBatis 动态 SQL 的使用
- 7.1 if 标签
- 7.2 trim 标签
- 7.3 where 标签
- 7.4 set 标签
- 7.5 foreach 标签
 
前言
在软件开发领域,持久层框架的选择对于项目的实现和维护起着至关重要的作用。MyBatis 作为一款优秀的持久层框架,以其灵活性、高度可定制化以及对SQL的直接控制等特性而广受关注和应用。本文将深入探索 MyBatis 框架,从初识到实际应用,逐步揭示其在现代软件开发中的关键作用。
一、初识 MyBatis
1.1 什么是 MyBatis
MyBatis 是一个优秀开源的 Java 持久层框架,用于简化数据库访问和操作的过程。它允许开发者使用简单的XML或注解配置来映射 Java 对象与数据库表之间的关系,从而实现数据库的持久化操作。MyBatis并不是一个全面的ORM(对象关系映射)框架,而是更强调对 SQL 的精确控制,使开发者能够更直接地编写和优化 SQL 语句。
ORM(对象关系映射)框架:
ORM,全称为对象关系映射(Object-Relational Mapping),是一种软件技术,用于将面向对象的编程语言(如Java、Python等)中的对象模型与关系型数据库中的数据模型之间进行映射和转换。简单来说,ORM框架允许开发者使用面向对象的思维来操作数据库,而不需要直接编写SQL语句。
MyBatis 的核心思想在于 SQL 的分解,它将 SQL 语句与 Java 代码分开,从而降低了代码的耦合度,提供了更大的灵活性和可维护性。通过配置映射文件(Mapper XML),开发者可以将SQL语句和查询结果的映射关系定义清晰,而Java代码则专注于业务逻辑的编写。此外,MyBatis还支持动态SQL、参数绑定、缓存等特性,使得数据库操作更加高效和便捷。
1.2 为什么学习 MyBatis
对于后端开发来说,程序是由以下两个重要的部分组成的,即 后端程序 和 数据库。

 这两个重要的组成部分要通讯,就要依靠数据库连接工具,比如之前的 JDBC 以及现在的 MyBatis 框架,都是为了连接并操作数据库。
 然而使用 JDBC 的操作会非常的繁琐,因此就需要使用其他更加简单高效的数据库连接方式了,而 MyBatis 就是一个更好的选择。
MyBatis 作为一个持久层框架,在现代软件开发中具有许多优势和价值,学习 MyBatis 的主要原因有:
1. 灵活的SQL 控制: MyBatis 允许开发者直接编写和控制 SQL 语句,这对于需要对数据库操作进行精确控制和优化的场景非常有用。开发人员可以编写自己的SQL语句,根据具体需求进行调整,而不受自动生成的SQL的限制。
2. 良好的性能: 由于开发者可以优化 SQL 语句,使其更适合特定的数据库和查询需求,因此 MyBatis 在性能方面表现出色。合理编写和优化的 SQL 语句可以显著提升应用程序的数据库访问效率。
3. 适应不同数据库: MyBatis 支持多种数据库,因此无论使用哪种关系型数据库(如MySQL、Oracle、SQL Server等),MyBatis 都可以适应并提供一致的操作方式。
4. 良好的扩展性: MyBatis 允许开发者编写自定义的 TypeHandlers、Plugins 等来满足特定需求,从而增强了框架的扩展性和定制性。
5. 轻量级框架: 相对于一些重量级的 ORM 框架,MyBatis 是一个相对轻量级的框架,学习成本较低,上手相对容易。
6. 可与其他框架集成: MyBatis 可以很容易地与其他流行的框架(如Spring、Spring Boot)进行集成,使得整体开发流程更加顺畅。
7. 更好地理解数据库: 通过学习 MyBatis,将不仅仅是在学习一个框架,还会更深入地理解数据库的工作方式和性能优化方法,这对于数据库设计和应用优化都有很大帮助。
二、MyBatis 在软件开发框架中的定位
理解 MyBatis 在整个软件开发框架中的定位是非常重要的,特别是对于了解其在系统架构中的作用和角色有帮助。下面是一个简单的交互流程图,展示了 MyBatis 在整个应用架构中的位置和交互关系:
 
在上述流程中,MyBatis主要位于持久层(Persistence),它的作用是将业务逻辑和数据库之间的交互进行封装和管理。下面是各层之间的交互关系:
-  前端界面:这是应用程序的用户界面,用户通过界面与系统进行交互,发送请求。 
-  控制层 (Controller):控制层接收来自用户界面的请求,处理请求的分发和调度,调用适当的服务层进行业务处理。 
-  服务层 (Service):服务层包含了应用程序的业务逻辑。它接收控制层传递的请求,处理业务逻辑,并可能需要与持久层进行数据交互。 
-  持久层 (Persistence - MyBatis):MyBatis 位于持久层,它负责将业务逻辑中的数据访问需求转化为对数据库的操作。通过映射文件(Mapper XML)和对应接口(Mapper Interface)进行关系映射,MyBatis 将 Java 对象和数据库表之间的数据转换进行管理。 
-  数据库 (DB):数据库是存储实际数据的地方。MyBatis通过SQL语句执行实际的数据库操作,将数据存储、检索、更新等操作反映到数据库中。 
在这个流程中,MyBatis 在持久层起到了桥梁的作用,负责将业务逻辑与数据库操作连接起来。它允许开发者通过映射文件或注解定义数据库表与 Java 对象之间的关系,从而实现数据的存取。这种定位使得开发者能够充分利用数据库的性能和功能,同时保持代码的可维护性和可扩展性。
三、基于 Spring Boot 创建 MyBatis 项目
3.1 添加 MyBatis 框架的支持
- 创建 Spring Boot 项目

- 添加 MyBatis 依赖

在创建 Spring Boot 项目的时候,如果想要创建 MyBatis 项目,需要在依赖中勾选MyBatis Framework,除此之外,还需要勾选一个具体的数据库驱动,比如MySQL Driver。
3.2 配置数据库连接信息和映射文件的保存路径(Mapper XML)
在创建好 Spring Boot 项目后,还需要在 application.yml 配置文件中为 MyBatis 配置数据库连接信息和映射文件的保存路径(Mapper XML)。
配置的内容如下:
# 配置数据库的连接字符串
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/database?characterEncoding=utf8
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
这部分配置用于设置数据库连接信息。需要根据实际情况修改url、username 和 password 字段,以连接到自己的 MySQL 数据库。其中driver-class-name 字段指定了MySQL数据库驱动程序的类名。
# 设置 Mybatis 的 xml 保存路径,在 resources/mapper 创建所有表的 xml ⽂件
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
这部分配置设置了 Mapper XML 文件的保存路径。mapper-locations 字段指定了 MyBatis 应该在classpath:mapper/ 路径下查找 Mapper XML文件。首先需要在这个路径下创建与MyMapper接口对应的Mapper XML文件,才能够使用 MyBatis。
四、MyBatis 项目结构的创建与使用
4.1 数据库和表的准备
此处创建一个userinfo表和 articleinfo表:
-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use mycnblog;
-- 创建表[用户表]
drop table if exists  userinfo;
create table userinfo(
    id int primary key auto_increment,
    username varchar(100) not null,
    password varchar(32) not null,
    photo varchar(500) default '',
    createtime timestamp default current_timestamp,
    updatetime timestamp default current_timestamp,
    `state` int default 1
) default charset 'utf8mb4';
-- 创建文章表
drop table if exists  articleinfo;
create table articleinfo(
    id int primary key auto_increment,
    title varchar(100) not null,
    content text not null,
    createtime timestamp default current_timestamp,
    updatetime timestamp default current_timestamp,
    uid int not null,
    rcount int not null default 1,
    `state` int default 1
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES 
(1, 'admin', 'admin', '', '2023-8-09 10:10:48', '2023-8-09 10:10:48', 1);
-- 文章添加测试数据
insert into articleinfo(title,content,uid) values('Java','Java正文',1);
insert into articleinfo(title,content,uid) values('C++','C++正文', 1);
insert into articleinfo(title,content,uid) values('Python','Python', 1);
insert into articleinfo(title,content,uid) values('PHP','PHP正文', 1);
4.2 根据数据库表创建实体类
例如针对表 userinfo 创建一个实体类:
@Data
public class UserInfo {
    private Integer id;
    private String username;
    private String password;
    private String photo;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private Integer state;
}
实体类中的属性名称,为了更好的兼容性,一般与数据库表中的字段相匹配。此处使用了 Lombok 库中的@Data 注解来自动生成实体类getter、setter、equals、hashCode 和 toString等方法,这样可以减少样板代码的编写。
4.3 创建 Mapper 接口和 XML 映射文件
上述实体类已经包含了与数据库表字段对应的属性,以及对应的数据类型。只需要确保在使用MyBatis时,Mapper 接口和 Mapper XML 文件与该实体类正确匹配。可以创建一个对应的Mapper接口和XML文件,然后使用@Mapper注解标记接口。
在 mapper 目录下创建 UserMapper 接口:

 其中,@Mapper 注解是 MyBatis 中的一个注解,用于标记一个接口为 MyBatis 的 Mapper 接口,从而告诉 MyBatis 这个接口定义了数据库操作的方法。在这个接口中,只需编写与数据库操作的相关代码即可,比如,获取所有的 User 信息:
import com.example.demo.entity.UserInfo;
import java.util.List;
@Mapper
public interface UserInfoMapper {
    List<UserInfo> getAll();
}
创建 XML 映射文件:
-  首先在 resources目录下创建一个mapper子目录,用于存放Mapper XML文件:
  
-  在这个 mapper路径下创建与UserInfoMapper.java接口对应的 XML 文件UserInfoMapper.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.example.demo.mapper.UserInfoMapper">
    
</mapper>
其中 namespace 字段指定的就是与 UserInfoMapper.xml 对应的 UserInfoMapper 接口的路径,此时便建立了 XML 文件与接口之间的映射关系。
当在 IDEA 中安装了 MyBatisX 插件,就可以发现出现了一对小鸟,此时点击 UserInfoMapper.xml边上的蓝色小鸟,就可以调整到与之映射的 UserInfoMapper 接口中。
 
 反之,点击UserInfoMapper 接口中的红色小鸟,也会跳转到与之对应的UserInfoMapper.xml文件中。
 
此时,发现接口中的方法会报错,那是因为在 Mapper XML文件中没有与之对应的 SQL 语句。
实现 getAll 方法对应的 SQL 语句:
在UserInfoMapper.xml编写查询所有用户的SQL语句:

 其中,id字段指定的是与这个 SQL 语句对应的 Mapper接口中方法,即getAll,而resultType 字段则是返回数据的类型,此处返回的是UserInfo对象,MyBatis 框架会根据这个映射配置,在查询执行完成后,将查询结果自动映射到 UserInfo 对象中。但是前提条件是,要确保实体类中的属性名称与数据库表的字段名称相匹配,这样 MyBatis 才能正确地进行结果映射。
4.4 创建服务层 Service 和 控制层 Controller
- 创建服务层 service目录,然后在该目录下创建UserInfoService类:
@Service
public class UserInfoService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    public List<UserInfo> getAll(){
        return userInfoMapper.getAll();
    }
}
- 创建控制层 controller目录,然后在该目录下创建UserInfoController类:
@RequestMapping("/user")
@RestController
public class UserInfoController {
    @Autowired
    private UserInfoService userInfoService;
    @GetMapping("/getAll")
    public List<UserInfo> getAll(){
        return userInfoService.getAll();
    }
}
此时已经完成了服务层和控制层的创建,其中控制层负责处理 HTTP 请求,以及与用户、服务层之间的交互;而服务层用于处理与用户信息相关的业务逻辑,并向控制层返回处理的结果。这种结构符合典型的三层架构(Controller - Service - Repository/DAO)设计模式,让代码更加清晰和易于维护。
在上述的代码中,UserInfoService 负责调用 UserInfoMapper 执行数据库操作,而 UserInfoController 则负责处理 HTTP 请求,将业务逻辑和数据库操作分离。
在这个基本的结构,允许通过访问/user/getAll来获取所有用户信息,例如此时运行服务器,然后在浏览器中输入http://localhost:8080/user/getAll进行访问,可以看到获取到了数据库中的所有用户信息:

五、通过 MyBatis 实现增、删、改操作
5.1 增加用户
1. 在UserInfoMapper 接口中添加一个 addUser 方法:
// 增加用户
int addUser(UserInfo user);
2. 在UserInfoMapper.xml中编写对应的 SQL 语句:
<insert id="addUser">
    insert into userinfo(username, password) values (#{username}, #{password})
</insert>
3. 此时,可以对 addUser 进行单元测试
1)首先在UserInfoMapper接口中点击右键,然后选择Generate:
 
 2)选择其中的Test:
 
 3)创建单元测试类

 此处选择添加测试addUser方法。添加完成后,可以在 test 目录下找到对应的测试类:

 4)编写测试代码

 简单说明:
- 在测试代码中,使用了@SpringBootTest注解,表示这是一个 Spring Boot 测试。
- @Autowired注解用于自动注入- UserInfoMapper,允许在测试中使用它。
- @Transactional注解用于表示测试过程中的事务操作,它会在测试结束时回滚,以避免对数据库造成实际的影响。
运行该测试代码,发现通过测试,则说明刚才的代码是正确的:
 
5.2 修改用户
例如,此时需要通过用户 id 来修改用户名:
1. 在UserInfoMapper 接口中添加一个 updateUserById 方法:
// 根据id修改用户名
int updateUserById(Integer id, String username);
2. 在UserInfoMapper.xml中编写对应的 SQL 语句:
<update id="updateUserById">
    update userinfo set username=#{username} where id=#{id}
</update>
3. 进行单元测试:
1)添加测试方法
@Test
void updateUserById() {
}
2)编写测试代码
此时 userinfo 表中的内容有:
 
 要求把 id 为 1 的用户名修改为 admin:
@Test
void updateUserById() {
    Integer id = 1;
    String username = "admin";
    int res = userInfoMapper.updateUserById(id, username);
    System.out.println("影响行数:" + res);
}
3)运行该测试方法
执行成功:
 
 再次查看 userinfo 表,发现已经成功进行了修改:
 
5.3 删除用户
现在,要求通过用户id删除指定用户:
1. 在UserInfoMapper 接口中添加一个 deleteUserById 方法:
// 根据 id 删除用户
int deleteUserById(Integer id);
2. 在UserInfoMapper.xml中编写对应的 SQL 语句:
<delete id="deleteUserById">
    delete from userinfo where id=#{id}
</delete>
3. 进行单元测试:
1)添加测试方法
@Test
void deleteUserById() {
}
2)编写测试方法
此时要删除 id 为 12 的用户:
    @Test
    void deleteUserById() {
        Integer id = 12;
        int res = userInfoMapper.deleteUserById(id);
        System.out.println("影响行数:" + res);
    }
3)运行测试代码
测试通过:
 
发现此时userinfo表中 id 为 12 的用户被删除了:

六、通过 MyBatis 实现查询操作
6.1 单表查询
6.1.1 通过用户 ID 查询
1. 在UserInfoMapper接口中添加getUserById方法:
// 根据id查询用户
UserInfo getUserById(Integer id);
2. 在UserInfoMapper.xml中编写对应的 SQL:
使用 #{} 参数占位符:
<select id="getUserById" resultType="com.example.demo.entity.UserInfo">
    select * from userinfo where id=#{id}
</select>
使用 ${} 参数占位符:
<select id="getUserById" resultType="com.example.demo.entity.UserInfo">
    select * from userinfo where id=${id}
</select>
3)进行单元测试
查询id 为 1 的用户:
@Test
void getUserById() {
    UserInfo user = userInfoMapper.getUserById(1);
    System.out.println(user);
}
使用 #{} 参数占位符的运行结果:

使用 ${} 参数占位符的运行结果:
 
 通过上述的测试代码不难发现:
- 在使用 #{}参数占位符的时候,准备执行的SQL语句中的参数位置为?,即经过了 SQL 的预编译,后面还需要对这个?进行赋值操作;
- 而使用 ${}参数占位符的时候参数是直接替换的。
6.1.2 参数占位符 #{} 和 ${}
在 MyBatis 中,#{} 和 ${} 是两种常用的参数占位符,用于在 SQL 语句中引用参数值。虽然它们看起来类似,但在使用时有一些重要的区别。
1. #{} 占位符:
- #{}占位符在 SQL 语句中使用时,会- 自动进行预编译,防止 SQL 注入攻击,并且能够处理参数的类型转换。它适用于大多数的 SQL 参数,如字符串、数字等。
2. ${} 占位符:
- ${}占位符在 SQL 语句中使用时,会- 将参数值直接嵌入到 SQL 语句中,不进行预编译。这可能会导致 SQL 注入风险,因此需要谨慎使用。它适用于一些特殊的场景,如动态表名或列名等。
因此,在大多数情况下推荐尽可能使用 #{} 占位符,以确保 SQL 的安全性和可维护性。只在必要的情况下使用 ${} 占位符,同时保证输入参数的合法性和安全性。
6.1.3 SQL 注入问题
下面通过使用
${}模拟登录时发生的 SQL 注入问题:
1. 在UserInfoMapper接口中添加getUserById方法:
// 实现登录操作
UserInfo login(UserInfo user);
2. 在UserInfoMapper.xml中编写对应的 SQL:
<select id="login" resultType="com.example.demo.entity.UserInfo">
	select * from userinfo where username='${username}' and password='${password}'
</select>
由于使用${}是直接进行参数替换的,因此需要在${}外面加上''。
3. 编写单元测试
首先进行正常的演示:
@Test
void login(){
    String username = "zhangsan";
    String password = "123456";
    UserInfo user = new UserInfo();
    user.setUsername(username);
    user.setPassword(password);
    UserInfo loginUser = userInfoMapper.login(user);
    System.out.println(loginUser);
}
此时可以成功获取到对象:
 
但是如果将 password 改成:
String password = "'  or 1='1";
再次运行测试代码:

 发现此时获取到了数据库中的全部内容,其执行的 SQL 语句是:
select * from userinfo where username='zhangsan' and password='' or 1='1'
即不管输入的username 和 password 是否正确,where 条件始终为 true,这就是 SQL 注入带来的风险。
如果此时将 ${} 改为 #{}:
<select id="login" resultType="com.example.demo.entity.UserInfo">
	select * from userinfo where username=#{username} and password=#{password}
</select>
再次运行刚才的代码:
 
此时通过预编译然后再获取参数,避免了 SQL 注入带来的风险。
6.1.3 like 查询
使用 like 通过用户名模糊查询:
 1. 在UserInfoMapper接口中添加getListByName方法:
// like 模糊查询
List<UserInfo> getListByName(@Param("username") String username);
2. 在UserInfoMapper.xml中编写对应的 SQL:
使用#{}参数占位符:
<select id="getListByName" resultType="com.example.demo.entity.UserInfo">
    select * from userinfo where username like '%#{username}%'
</select>
此时通过单元测试,发现最后会报错:

 这是因为当使用#{}时,最终形成的 SQL 语句是:
select * from userinfo where username like '%'ang'%'
而这是一条错误的 SQL 语句,所有会报错,因此使用 like 查询的时候需要使用${}参数占位符进行直接替换。
<select id="getListByName" resultType="com.example.demo.entity.UserInfo">
    select * from userinfo where username like '%${username}%'
</select>
再次运行测试代码,发现可以成功查找了:

 但是这样还是存在 SQL 注入问题,所以还是需要使用 #{},对于这种情况,可以使用 MySQL 的内置函数 concat 来解决:
<select id="getListByName" resultMap="BaseMap">
    select *
    from userinfo
    where username like concat('%', #{username}, '%');
</select>
其中,concat的作用就是拼接字符串,并且支持可变参数。
6.1.4 使用 resultMap 解决实体类参数与数据库表字段不匹配问题
有时候,我们程序中实体类中的参数名可能会和数据库表中的字段名不匹配,那么 MyBatis 就无法正确绑定查询结果到实体类对象了,此时可以使用 Mapper XML 中的 resultMap 来解决。
例如,userinfo表中的密码字段为 password,而实体类中的属性名为 pwd,此时再通过getUserById来查询用户,最后发现pwd属性为空:

 此时,在UserInfoMapper.xml文件中新加入一个 resultMap 标签:
 
简单说明:
-  <id>元素:定义了主键的映射。column属性指定数据库表的列名,property属性指定实体类的属性名。在这个示例中,数据库表的主键列 “id” 映射到实体类的属性 “id”。
-  <result>元素:定义了普通列的映射。column属性指定数据库表的列名,property属性指定实体类的属性名。在这个示例中,数据库表的 “username” 列映射到实体类的属性 “username”,“password” 列映射到实体类的属性 “pwd”,“photo” 列映射到实体类的属性 “photo”。
然后修改 getUserById 方法对应的 SQL,修改其返回结果为字典映射 baseMap:
<select id="getUserById" resultMap="baseMap">
    select * from userinfo where id=${id}
</select>
再次运行测试代码,就可以拿到正确的结果了:
当然,也可以在 SQL 语句中,将 password 重命名为 pwd 来解决这个问题,例如:
<select id="getUserById" resultType="com.example.demo.entity.UserInfo">
    select id, username, password as pwd, photo, createtime, updatetime, state 
    from userinfo where id=${id}
</select>
此时同样可以拿到正确的结果:
 
6.2 多表查询
6.2.1 VO类的创建
在进行代表查询的时候,通常都需要创建一个值对象(VO,Value Object)来包含多个表的相关信息。VO类是一个 Java 类,通常用于封装多个实体类的属性,从而方便在多个表查询中传递和处理数据。
例如,此时需要通过文章 id 来查询文章详情,而文章详情中需要包含用户名,但articleInfo 表中只有用户 uid,所有就需要进行多表查询。为了方便将用户名和文章信息相结合,因此就需要额外创建一个ArticleInfoVO类。
首先创建 articleinfo 表对应的实体类 ArticleInfo:
 
然后继承该类,在vo目录下创建一个 ArticleInfoVO 类:
 
6.2.2 创建 Mapper 接口和 XML 映射文件
1. 创建 Mapper 接口 ArticleInfoVOMapper:
 
 2. 创建 XML 映射文件 ArticleInfoVOMapper.xml:
 
6.2.3 查询文章详情
1. 在 ArticleInfoVOMapper接口中创建方法getDetial:
// 通过文章 id 查询文章详情
ArticleInfoVO getDetial(Integer id);
2. 在 ArticleInfoVOMapper.xml文件中编写对应的 SQL 语句:
<select id="getDetial" resultType="com.example.demo.entity.vo.ArticleInfoVO">
    select a.*, u.username from articleinfo a 
        left join userinfo u on a.uid = u.id 
                           where a.id = #{id}
</select>
3. 编写单元测试
@SpringBootTest
class ArticleInfoVOMapperTest {
    @Autowired
    private ArticleInfoVOMapper articleInfoVOMapper;
    @Test
    void getDetail() {
        ArticleInfoVO detail = articleInfoVOMapper.getDetail(1);
        System.out.println(detail);
    }
}
运行测试代码,发现能正确查找出结果:
 
七、MyBatis 动态 SQL 的使用
MyBatis 动态 SQL 是指根据不同的条件和参数,动态地生成 SQL 查询或更新语句的过程。它允许在编写 SQL 映射文件时,根据业务需求来动态组装 SQL 语句的各个部分,从而实现更灵活的数据库操作。动态 SQL 在处理不同的查询条件、排序、过滤等方面非常有用,它可以避免因为多种情况而编写大量重复的 SQL 语句,从而提高开发效率。
MyBatis 提供了一系列的 XML 标签和语法,用于构建动态 SQL。这些标签可以用来包含条件判断、循环遍历、动态拼接 SQL 片段等操作。一些常用的动态 SQL 标签包括 <if>、<choose>、<when>、<otherwise>、<trim>、<where>、<set>、<foreach> 等,详情可以参考MyBatis 官网: 动态SQL。
总之,MyBatis 动态 SQL 是一种强大的机制,使得在 SQL 映射文件中根据不同情况生成合适的 SQL 语句变得更加灵活和方便。下面是对一些常见的动态 SQL 标签的详细介绍。
7.1 if 标签
<if> 标签用于在 SQL 语句中添加条件判断,根据条件的真假来动态生成 SQL 片段。
例如,在添加用户信息的时候,photo 字段的内容可能不确定用户是否输入,这时就需要使用 <if> 标签来构建动态 SQL:
<insert id="addUser">
    insert into userinfo(
    username,
    <if test="photo!=null and photo!=''">
        photo,
    </if>
    password
    )
    values (
    #{username},
    <if test="photo!= null and photo!=''">
        #{photo},
    </if>
    #{pwd}
    )
</insert>
需要注意的是,其中 <if>标签中的 test 属性指定的是传入的对象的属性,而不是数据库表中的字段。
在单元测试中,只输入 username 和 password,最后形成的 SQL 也只有这两个字段:
 
 如果在增加输入一个photo 属性:

 可以发现此时三个字段都有。
7.2 trim 标签
如果当输入的所有属性都是可选的情况下,那么只使用 <if> 标签就不能解决其中的 , 问题了,因为不知道,在哪个位置出现,可以出现在前面,也可能在后面,如果没有输入的话可能都不出现。因此,要解决这个问题就需要引入<trim>标签。
<trim>标签属性:
- prefix:表示整个语句块,以- prefix的值作为前缀
- suffix:表示整个语句块,以- suffix的值作为后缀
- prefixOverrides:表示整个语句块要去除掉的前缀
- suffixOverrides:表示整个语句块要去除掉的后缀
例如,此时设置添加用户时的 username、password、photo 三个字段都是可选的:
<insert id="addUser">
    insert into userinfo
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="username!=null and username!=''">
            username,
        </if>
        <if test="photo!=null and photo!=''">
            photo,
        </if>
        <if test="pwd!=null and pwd!=''">
            password,
        </if>
    </trim>
    values
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="username!=null and username!=''">
            #{username},
        </if>
        <if test="photo!=null and photo!=''">
            #{photo},
        </if>
        <if test="pwd!=null and pwd!=''">
            #{pwd},
        </if>
    </trim>
</insert>
其中,<trim> 标签的作用就是用于修剪插入的列名和值部分,可以在开始和结束位置删除多余的逗号。prefix 属性表示在 SQL 片段前添加的内容,suffix 属性表示在 SQL 片段后添加的内容,suffixOverrides 属性表示在 SQL 片段结尾删除的内容。
7.3 where 标签
<where> 标签用于将条件添加到 SQL 语句的 WHERE 子句中,并处理条件之间的逻辑。
例如,现在可以通过文章的 id 或者 title 来进行查询,其中 id 和 title 的内容都是可选输入项,并且title使用的是模糊匹配。
<select id="getListByIdOrTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
    select a.*, u.username from articleinfo a
    left join userinfo u on a.uid = u.id
    <where>
        <if test="id != null and id > 0">
            and a.id = #{id}
        </if>
        <if test="title!=null and title!=null">
            and a.title like concat('%', #{title}, '%')
        </if>
    </where>
</select>
另外,<where>标签会自动去除前缀的and。
当然,也可以使用 <trim> 和 <if> 标签来实现这个功能:
<select id="getListByIdOrTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
    select a.*, u.username from articleinfo a
    left join userinfo u on a.uid = u.id
    <trim prefix="where" prefixOverrides="and">
        <if test="id != null and id > 0">
            and a.id = #{id}
        </if>
        <if test="title!=null and title!=null">
            and a.title like concat('%', #{title}, '%')
        </if>
    </trim>
</select>
此时需要使用<trim>标签,用来去除一个前缀and,以及添加一个前缀where。
7.4 set 标签
<set> 标签用于在更新语句中设置需要更新的字段,并根据条件动态生成更新语句。
例如,通过用户 id 来修改该用户不为 null 的属性:
<update id="updateById">
    update userinfo
    <set>
        <if test="username!=null and username!=''">
            username=#{username},
        </if>
        <if test="pwd!=null and pwd!=''">
            password=#{pwd},
        </if>
        <if test="photo!=null and photo!=''">
            photo=#{photo},
        </if>
    </set>
    where id=#{id}
</update>
<set>标签和<where> 相反,它只会去除后缀的, 。
7.5 foreach 标签
<foreach> 标签用于遍历集合或数组,并将其中的元素添加到 SQL 语句中。
<foreach>标签有如下属性:
- collection:绑定⽅法参数中的集合,如 List,Set,Map或数组对象
- item:遍历时的每⼀个对象
- open:语句块开头的字符串
- close:语句块结束的字符串
- separator:每次遍历之间间隔的字符串
例如,现在需要根据多个文章 id来删除对应的文章:
1. 在ArticleInfoVOMapper接口中添加方法:
// 根据多个文章 `id`来删除对应的文章
int deleteByIds(List<Integer> ids);
2. 在ArticleInfoVOMapper.xml编写对应SQL:
<delete id="deleteByIds">
    delete from articleinfo where id in
    <foreach collection="ids" item="item" open="(" close=")" separator=",">
        #{item}
    </foreach>
</delete>
这段代码演示了使用 MyBatis 的动态 SQL 构建删除语句的示例。这个删除语句会根据给定的 ID 列表,动态地生成 DELETE 语句中的 IN 子句,从而批量删除满足条件的记录。
简单说明:
-  <delete>标签:这个标签表示一个删除语句的定义。
-  <foreach>标签:这个标签用于遍历集合,将集合中的元素添加到 SQL 语句中。在这个示例中,它会将ids集合中的每个元素添加到IN子句中,形成类似(id1, id2, id3)的结构。- collection属性:指定要遍历的集合。
- item属性:指定在遍历过程中每个元素的别名。
- open属性:指定遍历开始时的字符,这里是- (。
- close属性:指定遍历结束时的字符,这里是- )。
- separator属性:指定元素之间的分隔符,这里是逗号- ,。
 
通过这种方式,可以使用动态 SQL 构建批量删除语句,根据给定的 ID 集合删除相应的记录。
3. 进行单元测试:
@Transactional
@Test
void deleteByIds() {
    List<Integer> ids = new ArrayList<>();
    ids.add(1);
    ids.add(2);
    ids.add(3);
    ids.add(4);
    int res = articleInfoVOMapper.deleteByIds(ids);
    System.out.println("影响行数:" + res);
}
测试通过:



















