一. 前言
在应用分层学习时,我们了解到web应永程序⼀般分为三层,即:Controller、Service、Dao. 之前的案例中,请求流程如下:浏览器发起请求,先请求Controller,Controller接收到请求之后,调用 Service进行业务逻辑处理,Service再调用Dao,但是Dao层的数据是Mock的,真实的数据应该从数据库 中读取.我们学习MySQL数据库时,已经学习了JDBC来操作数据库,但是JDBC操作太复杂了.
我们不但要拼接每⼀个参 数,⽽且还要按照模板代码的⽅式,⼀步步的操作数据库,并且在每次操作完,还要⼿动关闭连接 等,⽽所有的这些操作步骤都需要在每个⽅法中重复书写.那有没有⼀种⽅法,可以更简单、更⽅便的 操作数据库呢? 答案是肯定的,这就是我们要学习 MyBatis 的真正原因,它可以帮助我们更方便、更快速的操作数据库.
二. 什么是MyBatis?
MyBatis是⼀款优秀的 持久层框架,用于简化JDBC的开发。
MyBatis本是 Apache的⼀个开源项目iBatis,2010年这个项目由apache迁移到了google code,并 且改名为MyBatis。2013年11月迁移到Github.
更多介绍,可查看 MyBatis 中文官网 (https://mybatis.net.cn/)
注意:我们下面所讲的MyBatis不是原生的MyBatis,而是MyBatis 官方专门给 SpringBoot 做的整合包: mybatis-spring-boot-starter
三. MyBatis 入门
Mybatis操作数据库的步骤:
-
准备⼯作(创建springboot⼯程、数据库表准备、实体类)
-
引⼊Mybatis的相关依赖,配置Mybatis(数据库连接信息)
-
编写SQL语句(注解/XML)
-
测试
3.1 准备工作
3.1.2 创建springboot工程
创建springboot⼯程,并导⼊mybatis的起步依赖、mysql的驱动包

Mybatis 是⼀个持久层框架, 具体的数据存储和数据操作还是在MySQL中操作的, 所以需要添加 MySQL驱动
3.1.2 数据库表准备
sql
-- 创建数据库
DROP DATABASE IF EXISTS mybatis_test;
CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
USE mybatis_test;
-- 创建表[用户表]
DROP TABLE IF EXISTS user_info;
CREATE TABLE `user_info` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`username` VARCHAR ( 127 ) NOT NULL,
`password` VARCHAR ( 127 ) NOT NULL,
`age` TINYINT ( 4 ) NOT NULL,
`gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1-男 2-女 0-默认',
`phone` VARCHAR ( 15 ) DEFAULT NULL,
`delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
-- 添加用户信息
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );
在下面我们使用 Navicat 这个数据库图形化管理工具
这样我们就可以在数据库中得到一张表

3.1.3 创建对应的实体类 UserInfo
注意:数据库属性命名规范:小写+下划线。 Java属性命名规范:驼峰
在我们为数据库中的一个表设置对应的Java实体类时,如果数据库中属性名为小写,对应Java实体类属性名要完全对应上,如果数据库中属性名为小写+下划线,对应Java实体类属性名可以命名为驼峰,因为Mybatis有办法帮我们转换(后面会讲到)
java
package com.example.Mybatis.entity;
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {
private Integer id;
private String username;
private String password;
private Integer age;
private String phone;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
注意:我们在为数据库表设计对应java实体类时,该类中每个属性都要有对应的set方法(我们这里是使用lombok工具中的@Data注解帮我们自动生成所有属性对应得set方法)
3.2 配置数据库连接字符串
Mybatis中要连接数据库,需要数据库相关参数配置
• MySQL驱动类
• 登录名
• 密码
• 数据库连接字符串
sql
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
3.3 写持久层代码
注意:在之前讲到过,数据库数据层的操作一般是以dao为包名命名的,但是我们使用MyBatis进行对数据库的操作时,我们一般将包名命名为 mapper(而不用dao)

java
package com.example.Mybatis.mapper;
import com.example.Mybatis.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info")//查询整个表
List<UserInfo> selectAll();
}
注意:
- Mybatis的持久层接口规范⼀般都叫XxxMapper
- 我们设定的是接口而不是类,因为设计成接口就可以不用写方法的具体实现,Mybatis框架会自动生成其实体类对象
- @Mapper注解:表示MyBatis中的Mapper接口,程序运行时,框架会自动动生成接口的实现类对象(代理对象),并给交Spring的IOC容器管理
- @Select注解:代表的就是select查询,也就是注解对应方法的具体实现内容.
3.4 单元测试
在创建出来的SpringBoot工程中,在src下的test目录下,已经⾃动帮我们创建好了测试类,我们可以 直接使用这个测试类来进行测试.

java
package com.example.Mybatis;
import com.example.Mybatis.entity.UserInfo;
import com.example.Mybatis.mapper.UserInfoMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class MybatisApplicationTests {
@Autowired
UserInfoMapper userInfoMapper;
@Test
void contextLoads() {
List<UserInfo> userInfoList=userInfoMapper.selectAll();
System.out.println(userInfoList);
}
}
@Test 注解是单元测试代码中的重要注解:
@Test
- 作用
用来标记一个普通方法为测试方法,让测试框架(如 JUnit、SpringBoot Test)可以自动识别并运行这个方法。我们直接点击 @Test 左边的三角标志 ,即可执行该方法(不用写main方法)
- 好处
不用写 main 方法就能运行代码
专门用来测试:Mapper、Service、Controller 等功能
运行快、不启动整个项目、方便排查错误
@SpringBootTest 注解也是单元测试的重要注解:
@SpringBootTest 的作用:
启动了Spring的容器,Spring 容器 里面的Bean我们都可以直接注入使用(启动了才能注入)
让测试中可以使用 @Autowired 自动注入(Mapper、Service、Controller 都能注入)
提供和真实项目一样的运行环境,方便测试业务代码
点击@SpringBootTest 左边的三角标志,即可执行该类中的所有带有@Test注解的方法
测试类上添加了注解 @SpringBootTest,该测试类在运行时,就会自动加载Spring的运行环境. 我们通过@Autowired这个注解, 注入我们要测试的类, 就可以开始进行测试了。
使用Idea自动生成测试类
除此之外,也可以使用Idea自动生成测试类



即可生成:

注意:我们使用 IDEA 自动生成的测试类,是对应需要测试的那个业务类的。
以后凡是要测试这个类里的方法,统一都写在这个对应的测试类中。
四. MyBatis的基础操作
4.1 打印日志
在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果在配置文件中进行配置即可
java
mybatis:
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
未加该配置前:(测试selectAll()方法)

加之后:(测试selectAll()方法)

通过MyBatis日志配置,我们就可以直观看到执行的 SQL 语句、参数、返回结果。(日志里打印的结果,就是数据库直接返回的原始数据,还没经过 MyBatis 封装成 Java 对象之前的样子。数据库里的表是什么样子,日志打印出来就是什么样子。)

==> 是"发出去"的 SQL 和参数
<== 是"收回来"的列名、行数据和总行数
Columns: :查询结果的列名列表。
Row: :查询返回的每一行原始数据。
Total: :返回的总行数。
4.2 参数传递
例子:
需求:从数据中的的user_info中查找 id=?的用户,这个id就是需要我们给它传递参数
解决方案:将方法中的参数,传给SQL语句使用 #{ } 的方式获取方法中的参数
方法编写:
java
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info where id=#{id}")
UserInfo selectById(Integer id);
}
测试方法编写:
java
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void selectById() {
UserInfo userInfo=userInfoMapper.selectById(3);
System.out.println(userInfo);
}
}
当我们传入参数时,即id=3时,就会将参数中的id传入 SQL语句中的 #{id} (#{ } 就相当于一个占位符)
注意:
- 当参数只有一个是方法的参数名可以和 #{ } 中的名称不一致,MyBatis也能识别,当有两个/多个参数时,两者就要完全一致,不然会报错
- 也可以通过 @Param ,设置参数的别名,如果使用 @Param 设置别名, #{...} 里面的属性名必须和 @Param 设置的⼀样(@Param 的作用就是:把方法里的参数和别名绑定在一起。)
java
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info where id=#{userId}")
UserInfo selectById(@Param("userId") Integer id);//通过@Param起别名
@Select("select * from user_info")
List<UserInfo> selectAll();
}
//@Param("userId") Integer id 就是把 id 和 userId 绑定。
//之后在 @Select 的 SQL 里,就用 #{userId} 来对应这个别名,MyBatis 就知道要取 id 的值了。
- @select 注解中的内容通过MyBatis处理后就是直接要交给数据库执行的原生SQL语句
执行测试方法:

4.3 增(Insert)
SQL语句:
sql
insert into user_info (username, `password`, age, gender, phone) values
("zhaoliu","zhaoliu",19,1,"18700001234")
把SQL中的常量替换为动态的参数
Mapper接口:
java
@Mapper
public interface UserInfoMapper {
List<UserInfo> selectAll();
@Insert("insert into user_info (username, `password`, age, gender, phone)"+
" values (#{username},#{password},#{age},#{gender},#{phone})")
Integer insert(UserInfo userInfo);//当参数有多个时,可以设置为对象
}
测试代码:
java
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void insert() {
UserInfo userInfo=new UserInfo();
userInfo.setUsername("zhaoliu");
userInfo.setPassword("zhaoliu");
userInfo.setGender(2);
userInfo.setAge(21);
userInfo.setPhone("18612340005");
Integer a=userInfoMapper.insert(userInfo);
System.out.println("影响行数"+a);
System.out.println(userInfo);
}
执行结果:

补: 为什么我们在为数据库表设计对应java实体类时,为什么没有将这个实体类交给Spring管?
答: Mapper 中的每个方法,即便最终返回的数据结构相同,用于承载数据的实体类对象也是相互独立的。实体类之所以不需要交给 Spring 管理,是因为它仅作为数据载体,主要出现在方法的参数与返回值位置,只负责封装和传输数据,不包含任何业务行为与逻辑处理,因此无需由 Spring 容器创建、管理和依赖注入
如果我们为对象参数使用 @Param 起别名时,#{...} 需要使用 参数.属性 来获取,代码如下:
java
@Mapper
public interface UserInfoMapper {
List<UserInfo> selectAll();
@Insert("insert into user_info (username, `password`, age, gender, phone)"+
" values (#{UI.username},#{UI.password},#{UI.age},#{UI.gender},#{UI.phone})")
Integer insert(@Param("UI") UserInfo userInfo);
}
从上面的代码可以清晰地看出,MyBatis 的工作流程是:
-
首先从我们传入的参数(即 UserInfo 对象)中,通过 UI.username 、 UI.password 等表达式提取出对应字段的值。
-
然后将这些提取到的数据,填充到预编译的 SQL 语句中,最终执行
返回主键
Insert 语句默认返回的是受影响的行数
但有些情况下,数据插⼊之后,还需要有后续的关联操作,需要获取到新插⼊数据的id。比如订单系统 当我们下完订单之后,需要通知物流系统,库存系统,结算系统等,这时候就需要拿到订单ID
如果想要拿到增id,需要在Mapper接的方法上添加⼀个Options的注解
java
@Mapper
public interface UserInfoMapper {
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into user_info (username, `password`, age, gender, phone)"+
" values (#{UI.username},#{UI.password},#{UI.age},#{UI.gender},#{UI.phone})")
Integer insert(@Param("UI") UserInfo userInfo);
}
解释:
|------------------|------|---------------------------------|
| 参数名 | 取值 | 含义 |
| useGeneratedKeys | true | 开启"获取数据库自动生成主键"的功能(默认值为 false ) |
| keyProperty | "id" | 指定将生成的主键值,赋值给传入实体类对象的哪个属性 |
核心好处:无需额外执行 SELECT 语句获取自增 ID
在传统方式下,插入一条新记录后,若要获取数据库自动生成的自增主键 ID,必须再执行一次 SELECT LAST_INSERT_ID() 或类似的查询语句,这会增加一次数据库交互。
而通过 @Options(useGeneratedKeys = true, keyProperty = "id") ,MyBatis 会在插入操作的同一数据库连接和事务上下文中,自动将生成的主键值回填到传入的实体对象中。
注意:设置 useGeneratedKeys=true 之后,方法返回值依然是受影响的行数,自增id 会设置在上述 keyProperty 指定的属性中.
测试数据:
java
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void insert() {
UserInfo userInfo=new UserInfo();
userInfo.setUsername("zhaoliu");
userInfo.setPassword("zhaoliu");
userInfo.setGender(2);
userInfo.setAge(21);
userInfo.setPhone("18612340005");
Integer count=userInfoMapper.insert(userInfo);
System.out.println("影响行数:"+count+"自增id= "+userInfo.getId());
}
运行结果:

4.4 删(Delete)
SQL语句:
sql
delete from user_info where id=6
把SQL中的常量替换为动态的参数
Mapper接口:
java
@Mapper
public interface UserInfoMapper {
@Delete("delete from user_info where id=#{id}")
Integer DeleteById(Integer id);
}
}
注意:Delete操作返回值含义:表示本次操作成功影响到的数据库行数
4.5 改(Update)
SQL语句:
sql
update user_info set username="zhaoliu" where id=5
把SQL中的常量替换为动态的参数
Mapper接口:
java
@Mapper
public interface UserInfoMapper {
@Update("update user_info set username=#{username} where id=#{id}")
Integer update(UserInfo userInfo);
}
测试数据:
java
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void update() {
UserInfo userInfo=new UserInfo();
userInfo.setUsername("aaa");
userInfo.setId(5);
Integer count=userInfoMapper.update(userInfo);
}

4.6 查 (Select)
我们在上⾯查询时发现,有几个字段是没有赋值的, 只有Java对象属性和数据库字段⼀模⼀样时, 才会进行赋值 接下来我们多查询⼀些数据
java
@Mapper
public interface UserInfoMapper {
@Select("select id, username, `password`, age, gender, phone, delete_flag,create_time, update_time from user_info")
List<UserInfo> queryAllUser();
}
测试数据:
java
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void queryAllUser() {
List<UserInfo> userInfos=userInfoMapper.queryAllUser();
System.out.println(userInfos);
}
运行结果:

从运行结果上可以看到,我们SQL语句中,查询了delete_flag, create_time, update_time,但是这几个 属性却没有赋值.
核心原因:
MyBatis 在进行结果映射时,默认采用的是严格的名称匹配规则:
数据库字段名: delete_flag 、 create_time 、 update_time (下划线命名法)
Java 对象属性名: deleteFlag 、 createTime 、 updateTime (驼峰命名法)
由于两者名称不完全一致,MyBatis 无法自动建立映射关系,因此这些字段的值无法被正确赋值到对象属性中,导致它们为 null 。
在执行 SELECT 操作时,MyBatis 的工作流程如下:
执行 SQL:首先执行 @Select 注解或 XML 中定义的 SQL 语句,从数据库中获取到结果集(ResultSet)。
结果映射:遍历结果集中的每一行数据,将其转换为对应的 Java 实体类对象。
名称匹配:在映射过程中,MyBatis 会严格按照名称匹配的规则,将数据库字段名与 Java 实体类的属性名进行一一对应。
只有当两者的名称完全一致时,才会将数据库字段的值成功赋值给对应的实体类属性。
如果名称不一致(例如数据库使用下划线命名 delete_flag ,而 Java 使用驼峰命名 deleteFlag ),MyBatis 无法自动建立映射关系,就会导致该属性值为 null ,即映射失败。
解决方案:
- 法一:起别名
java
@Select("select id, username, `password`, age, gender, phone, delete_flag as deleteFlag, +" +
" create_time as createTime, update_time as updateTime from user_info")
List<UserInfo> queryAllUser();
注:SQL语句太长时,使用加号 + 进行字符串拼接
运行结果:(这样就赋值成功了)

- 法二:结果映射---使用 @Results 注解定义映射关系
java
@Select("select id, username, `password`, age, gender, phone, delete_flag, " +
" create_time, update_time from user_info")
@Results({
@Result(column = "delete_flag",property = "deleteFlag"),
@Result(column = "create_time",property = "createTime"),
@Result(column = "update_time",property = "updateTime")
})
List<UserInfo> queryAllUser();
@Results 本质上就是一个注解容器,专门用来存放多个 @Result 注解。每一个@Result 注解就定义了一条映射关系,其中


如果其他SQL,也希望可以复用这个映射关系,可以给这个Results定义⼀个名称,使用 id 属性给该 Results 定义别名,使用 @ResultMap 注解来复用其他定义的 ResultMap :
java
@Select("select id, username, `password`, age, gender, phone, delete_flag," +
" create_time, update_time from user_info")
@Results(id="resultMap1",value = {
@Result(column = "delete_flag",property = "deleteFlag"),
@Result(column = "create_time",property = "createTime"),
@Result(column = "update_time",property = "updateTime")
})
List<UserInfo> queryAllUser();
@Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time " +
"from user_info where id= #{userid} ")
@ResultMap(value = "resultMap1")//复用映射关系
UserInfo queryById(@Param("userid") Integer id);
- 法三:开启驼峰命名(推荐)
通常数据库列使用蛇形命名法进行命名(下划线分割各个单词), 而 Java 属性⼀般遵循驼峰命名法约定. 为了在这两种命名方式之间启用动映射,需要将 mapUnderscoreToCamelCase 设置为 true。
java
mybatis:
configuration:
map-underscore-to-camel-case: true #配置驼峰⾃动转换
Java代码不做任何处理
java
@Select("select id, username, `password`, age, gender, phone, delete_flag," +
" create_time, update_time from user_info")
List<UserInfo> queryAllUser();
五. MyBatis XML配置文件
Mybatis的开发有两种方式:
-
注解(我们上面讲到的 MyBatis的基础操作)
-
XML
上面学习了注解的方式,接下来我们学习XML的方式
MyBatis XML的方式需要以下两步:
1. 配置数据库连接字符串和MyBatis
2. 写持久层代码
5.1 配置连接字符串和MyBatis
此步骤需要进行两项设置,数据库连接字符串设置和 MyBatis 的 XML 文件配置。如果是application.yml文件, 配置内容如下:
python
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
注意:
- classpath:表示 resources 目录
- mapper目录和 **Mapper.xml文件都需要我们手动创建,IDEA 不会自动生成
- **Mapper.xml:表示我们创建的MyBatis XML文件名需要以Mapper结尾,以xml为文件后缀
5.2 写持久层代码
持久层代码分两部分
-
方法定义 Interface
-
方法实现: XXX.xml(相当于我们把之前注解的那部分内容写到了MyBatis 的 XML文件中)
5.2.1 添加 mapper 接口
mapper 接口中我们只写方法定义,不写方法实现
java
ackage com.example.Mybatis.mapper;
import com.example.Mybatis.entity.UserInfo;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserInfoXMlMapper {
List<UserInfo> queryAllUser();
}
5.2.2 添加 UserInfoXMLMapper.xml
添加 MyBatis XML文件需要和我们配置 mybatis xml 的文件路径一致

数据持久成的实现,MyBatis 的固定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="com.example.demo.mapper.UserInfoMapper">
</mapper>
注意:我们要生成哪个mapper接口的MyBatis XML文件,在mapper文件的 namespace 必须写:你要绑定的那个 Mapper 接口的全类名(包名 + 类名)
查询所有用户的具体实现:
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.Mybatis.mapper.UserInfoXMlMapper">
<select id="queryAllUser" resultType="com.example.Mybatis.entity.UserInfo">
select username,`password`, age, gender, phone from user_info
</select>
</mapper>
以下是对以上标签的说明:
标签:需要指定 namespace 属性,表示命名空间,值为对应 mapper接口的全限定名 包括全包名.类名。
• 查询标签:是用来执行数据库的查询操作的:
--id :是和Interface (接⼝)中定义的方法名称⼀样的,表示对接⼝的具体实现方法。
--resultType :是返回的数据类型,也就是开头我们定义的实体类.

5.2.3 单元测试
与使用注解进行的单元测试方式一致
java
@SpringBootTest
class UserInfoXMlMapperTest {
@Autowired
UserInfoXMlMapper userInfoXMlMapper;
@Test
void queryAllUser() {
List<UserInfo> userInfos=userInfoXMlMapper.queryAllUser();
System.out.println(userInfos);
}
}
5.3 增删改查操作
接下来,我们来实现⼀下⽤⼾的增加、删除和修改的操作.
5.3.1 增 (Insert)
UserInfoMapper接⼝:
java
@Mapper
public interface UserInfoXMlMapper {
Integer insertUser(UserInfo userInfo);
}
UserInfoMapper.xml实现:
XML
<insert id="insertUser">
insert into userinfo (username, `password`, age, gender, phone) values (#
{username}, #{password}, #{age},#{gender},#{phone})
</insert>
如果使用@Param设置参数名称的话,使用方法和注解类似
UserInfoMapper接⼝:
java
Integer insertUser(@Param("userInfo") UserInfo userInfo);
UserInfoMapper.xml实现:
XML
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into user_info (username, `password`, age, gender, phone) values
(#{userInfo.username},#{userInfo.password},#{userInfo.age},#
{userInfo.gender},#{userInfo.phone})
</insert>
返回自增 id
接⼝定义不变, Mapper.xml 实现 设置useGeneratedKeys 和keyProperty属性(与使用@Options注解类似)
XML
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into user_info (username, `password`, age, gender, phone) values
(#{userInfo.username},#{userInfo.password},#{userInfo.age},#
{userInfo.gender},#{userInfo.phone})
</insert>
5.3.2 删 (Delete)
UserInfoMapper接口:
java
Integer deleteUser(Integer id);
UserInfoMapper.xml实现:
XML
<delete id="deleteUser">
delete from user_info where id = #{id}
</delete>
5.3.3 改 (Update)
UserInfoMapper接⼝:
java
Integer updateUser(UserInfo userInfo);
UserInfoMapper.xml实现:
XML
<update id="updateUser">
update user_info set username=#{username} where id=#{id}
</update>
5.3.4 查 (Select)
同样的,使用XML 的方式进行查询,也存在数据封装的问题 。我们把SQL语句进⾏简单修改,查询更多的字段内容
XML
<select id="queryAllUser" resultType="com.example.demo.model.UserInfo">
select id, username,`password`, age, gender, phone, delete_flag,
create_time, update_time from user_info
</select>

结果显示: deleteFlag, createTime, updateTime 也没有进行赋值. 解决办法和注解类似:
-
起别
-
结果映射
-
开启驼峰命名
其中1,3的解决办法和注解⼀样,不再多说,接下来看下xml如果来写结果映射
XML
<resultMap id="BaseMap" type="com.example.Mybatis.entity.UserInfo">
<id column="id" property="id"></id>
<result column="delete_flag" property="deleteFlag"></result>
<result column="create_time" property="createTime"></result>
<result column="update_time" property="updateTime"></result>
</resultMap>
<select id="queryAllUser" resultMap="BaseMap">
select username,`password`, age, gender, phone from user_info
</select>

六. 其它查询操作
6.1 多表查询
多表查询和单表查询类似,只是SQL不同而已
6.1.1 准备工作
上面建了⼀张用户表,我们再来建⼀张文章表,进行多表关联查询. 文章表的uid,对应用户表的id.
sql
-- 创建⽂章表
DROP TABLE IF EXISTS article_info;
CREATE TABLE articleinfo (
id INT PRIMARY KEY auto_increment,
title VARCHAR ( 100 ) NOT NULL,
content TEXT NOT NULL,
uid INT NOT NULL,
delete_flag TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
create_time DATETIME DEFAULT now(),
update_time DATETIME DEFAULT now()
) DEFAULT charset 'utf8mb4';
-- 插⼊测试数据
INSERT INTO article_info ( title, content, uid ) VALUES ( 'Java', 'Java正⽂', 1 );

创建对应实体Java类
java
@Data
public class ArticleInfo {
private Integer id;
private String title;
private String content;
private Integer uid;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
6.1.2 数据查询
需求:根据uid查询作者的名称等相关信息
SQL:
sql
SELECT ta.id,ta.title,ta.content,ta.uid,tb.username,tb.age,tb.gender
FROM articleinfo ta LEFT JOIN user_info tb ON ta.uid = tb.id WHERE ta.id =1
补充实体类:
java
@Data
public class ArticleInfo {
private Integer id;
private String title;
private String content;
private Integer uid;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
//用户相关信息
private String username;
private Integer age;
private Integer gender;
}
接⼝定义:
java
@Mapper
public interface ArticleInfoMapper {
@Select("SELECT ta.id,ta.title,ta.content,ta.uid,tb.username,tb.age,tb.gender "+
"FROM article_info ta LEFT JOIN user_info tb ON ta.uid = tb.id WHERE ta.id =#{id}")
ArticleInfo queryUserByUid(Integer id);
}
6.2 #{ } 和 ${ }
MyBatis 参数赋值有两种方式,咱们前面使用了#{} 进行赋值,接下来我们看下二者的区别
6.2.1 #{ } 和 ${ } 使用
- 先看Interger类型的参数
使用 # { }:
java
@Mapper
public interface UserInfoMapper {
List<UserInfo> queryAllUser();
@Select("select * from user_info where id=#{id}")
UserInfo selectById(Integer id);
}

我们输入的参数并没有在后面拼接,id的值是使用 ? 进行占位. 这种SQL我们称之为"预编译SQL"
使用 ${ }:
java
@Mapper
public interface UserInfoMapper {
List<UserInfo> queryAllUser();
@Select("select * from user_info where id=${id}")
UserInfo selectById(Integer id);
}

可以看到,这次的参数是直接拼接在SQL语句中了.
- 接下来我们再看String类型的参数
java
@Mapper
public interface UserInfoMapper {
@Select("select username, `password`, age, gender, phone from user_info where username= #{name} ")
UserInfo queryByName(String name);
}

观察我们打印的日志,结果正常返回
我们把 #{} 改成 ${} 再观察打印的日志:
java
@Mapper
public interface UserInfoMapper {
@Select("select username, `password`, age, gender, phone from user_info where username= ${name} ")
UserInfo queryByName(String name);
}

可以看到,这次的参数依然是直接拼接在SQL语句中了,但是字符串作为参数时,需要添加引号 '' ,使用 ${} 不会拼接引号 '' ,导致程序报错.
修改代码如下:
java
@Mapper
public interface UserInfoMapper {
@Select("select username, `password`, age, gender, phone from user_info where username= '${name}' ")
UserInfo queryByName(String name);
}
再次运行,结果正确返回

从上面两个例子可以看出:
#{} 使用的是预编译SQL,通过 ? 占位的放式, 提前对SQL进行编译,然后把参数填充到SQL语句 中.#{} 会根据参数类型, 自动拼接引号'' .
${} 会直接进行字符替换, ⼀起对SQL进行编译. 如果参数为字符串, 需要加上引号 '' 。
6.2.2 #{ } 和 ${ } 的区别
#{} 和 ${}的区别就是 预编译SQL 和 即时SQL 的区别.
- 性能更高
绝⼤多数情况下,某⼀条SQL语句可能会被反复调用执行,或者每次执行的时候只有个别的值不同(比 如 select 的 where 子句值不同, update 的 set 子句值不同, insert 的 values 值不同).如果每次都需要 经过上面的语法解析,SQL优化、SQL编译等,则效率就明显不行了.

预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译 (只是输入的参数不同),省去了解析优化等过程,以此来提⾼效率
- 更安全(防止SQL注⼊)
SQL注入:是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法
由于没有对用户输⼊进行充分检查,而SQL又是拼接而成,在用户输⼊参数时,在参数中添加⼀些 SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。
先来看看SQL注⼊的例子:
java
@Mapper
public interface UserInfoMapper {
@Select("select username, `password`, age, gender, phone from user_info where username= '${name}' ")
UserInfo queryByName(String name);
}
正常访问传参: admin
java
@Test
void queryByName() {
UserInfo userInfo=userInfoMapper.queryByName("admin");
System.out.println(userInfo);
}
}
运行结果:

SQL注⼊场景: 传参 ' or 1='1

结果依然被正确查询出来了,其中参数 or 被当做了SQL语句的⼀部分
可以看出来,查询的数据并不是自己想要的数据.所以用于查询的字段,尽量使用 #{} 预查询的方式
SQL注入(SQL Injection)
一、定义
SQL注入是Web安全中最常见、危害最大的漏洞之一。
指攻击者在用户可控的输入点(如表单、URL参数、Cookie等),恶意插入/拼接SQL语句片段,欺骗后端数据库执行非预期的SQL命令,从而非法操作数据。
本质原因:
用户输入未做过滤/校验,直接拼接到SQL语句中执行。
二、主要危害
- 数据泄露
窃取用户信息、密码、手机号、身份证、订单、商业数据等。
- 数据篡改
删库、删表、改密码、改权限、改订单金额/状态。
- 越权访问
绕过登录、查看他人数据、获取管理员权限。
- 服务器沦陷
部分数据库可执行系统命令、读写文件,进而拿下服务器权限。
- 业务瘫痪
删库、拖库、导致系统崩溃、业务停服,造成经济与信誉损失。
- 法律风险
泄露用户隐私违反《网络安全法》《个人信息保护法》,企业需承担责任。
6.3 排序功能
从上面的例子中,可以得出结论: { }会有SQL注入的风险,所以我们尽量使用 #{ }完成查询 既然如此,是不是 { }就没有存在的必要性了呢?----当然不是.
接下来我们看下${}的使用场景:

SQL:
java
select id, username, age, gender, phone, delete_flag, create_time,
update_time from user_info order by id asc
ASC:升序(从小到大,默认可不写)
DESC:降序(从大到小)
ASC 和 DESC不能添加引号!!
由于ASC 和 DESC不能添加引号并且 #{} 会根据参数类型判断是否拼接引号 ''( 如果参数类型为String,就会加上引号.),所以这里就不可以使用 #{},而要使用 ${}
但是我们前面讲到过使用 ${} 会有SQL注入的风险,那么我们该如何预防呢-----限制用户输入,把参数设为枚举类型,只有枚举里允许的值才能传入。
6.4 like 查询
like 使用 #{ } 报错
java
@Mapper
public interface UserInfoMapper {
@Select("select id, username, age, gender, phone, delete_flag, create_time, update_time from user_info where username like '%#{key}%' ")
List<UserInfo> queryAllUserByLike(String key);
}
like 模糊查询
SQL语法:
sqlSELECT * FROM 表 WHERE 字段 LIKE '匹配串';
解决办法: 使用 mysql 的内置函数 concat() 来处理,实现代码如下:
java
@Select("select id, username, age, gender, phone, delete_flag, create_time,
update_time from user_info where username like concat('%',#{key},'%')")
List<UserInfo> queryAllUserByLike(String key);
七. 数据库连接池
在上⾯Mybatis的讲解中, 我们使用了数据库连接池技术, 避免频繁的创建连接,销毁连接 下 我们来了解下数据库连接池(以前我们是 每次操作都新建连接 → 用完关闭 ;现在用 连接池(DataSource):一开始就创建一批连接,循环复用,不频繁创建销毁。)
7.1 介绍
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用⼀个现有的数据库连接, 而不是再重新建立⼀个.

没有使用数据库连接池的情况: 每次执行SQL语句, 要先创建⼀个新的连接对象, 然后执行SQL语句, SQL 语句执行完,再关闭连接对象释放资源.这种重复的创建连接,销毁连接比较消耗资源。
使用数据库连接池的情况: 程序启动时,会在数据库连接池中创建⼀定数量的Connection对象,当客户请求数据库连接池,会从数据库连接池中获取Connection对象,然后执行SQL,SQL语句执行完,再把 Connection归还给连接池.
优点:
- 减少了网络开销
- 资源重用
- 提升了系统的性能
7.2 使用
常见的数据库连接池: • C3P0 • DBCP • Druid • Hikari
目前前较流行的是Hikari,Druid
其中 Hikari 是 SpringBoot默认使⽤的数据库连接池

如果我们想把默认的数据库连接池切换为Druid数据库连接池,只需要引⼊相关依赖即可
XML
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.21</version>
</dependency>
八. 总结:#{ } 和 ${ } 区别
- #{}:预编译处理, ${}:字符直接替换
- #{}可以防止SQL注入,${}存在SQL注⼊的风险,查询语句中,可以使用 #{},推荐使用#{}
- 但是⼀些场景,#{}不能完成,比如排序功能,表名,字段名作为参数时,这些情况需要使用${}
- 模糊查询虽然${}可以完成,但因为存在SQL注⼊的问题,所以通常使用mysql内置函数concat来完成
IDEA我还有一个工具:MyBatisX ,可以辅助我们更简洁,方便的使用MyBatis


