本节目标
- 使用MyBatis完成简单的增删改查操作,参数传递
- 掌握MyBatis的两种写法:注解和XML方式
- 掌握MyBatis相关的日志配置
前言
在应用分层学习中,我们了解web应用程序一般分为三层,即Controller、Service、Dao。在之前的案例中,请求流程如下:浏览器发起请求,先请求Controller,Controller接收到请求之后,调用Service进行业务逻辑处理,Service再调用Dao,但是Dao层的数据是Mock的,真实的数据应该从数据库中读取。
我们学习MySQL数据库时,已经学习了JDBC来操作数据库,但是JDBC操作太复杂了。
JDBC操作示例回顾
- 创建数据库连接池DataSource
- 通过DataSource获取数据库连接Connection
- 编写要执行带?占位符的SQL语句
- 通过Connection及SQL创建操作命令对象Statement
- 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值
- 使用Stament执行SQL语句
- 查询操作:返回结果集ResultSet,更新操作:返回更新的数量
- 处理结果集
- 释放资源
从上述流程可以看出,对于JDBC来说,整个操作非常的繁琐,那么有没有一种方法,可以更简单、更方便的操作数据库呢?
目录
一、什么是MyBatis?
- MyBatis是一款优秀的持久层框架,用于简化JDBC的开发
- MyBatis本是Apache的一个开源项目iBatis,2010年这个项目由apache迁移到google code,并改名为MyBatis。
持久层:指的就是持久化操作的层,通常指数据访问层(dao),用来操作数据库的。

简单来说MyBatis是更简单完成程序和数据库交互的框架,也就是更简单的操作和读取数据工具
二、MyBatis入门
MyBatis操作数据库的步骤:
- 准备工作(创建springboot工程、数据库表准备,实体类)
- 引入MyBatis的相关依赖,配置MyBtis(数据库连接信息)
- 编写SQL语句(注解/XML)
- 测试
2.1 准备工作
2.1.1 创建工程
创建springbboot工程,并导入MyBatis的依赖,MySQL的驱动包

2.1.2 数据准备
创建用户表,并创建对应的实体类UserInfo
-- 创建数据库
DROP DATABASE IF EXISTS mybatis_test;
CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
USE mybatis_test;
-- 创建表[用户表]
DROP TABLE IF EXISTS userinfo;
CREATE TABLE `userinfo` (
`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(),
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
-- 添加用户信息
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );
创建对应的实体类UserInfo
import lombok.Data;
import java.util.Date;
@Data
public class Userinfo {
private Integer id;
private String username;
private String password;
private Integer age;
private Integer gender;
private String phone;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
2.2 配置数据库连接字符串
MyBatis中要连接数据库,需要数据库相关参数配置
- MySQL驱动类
- 登录名
- 密码
- 数据库连接字符串
如果是application.yml文件,配置内容如下:
#数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
username: root
password: 111111
driver-class-name: com.mysql.cj.jdbc.Driver
2.3 写持久层代码
在项目中,创建持久层接口UserInfoMapper

import com.example.MyBatis.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo")
List<UserInfo> getUserInfoAll();
}
MyBatis的持久层接口规范一般都叫xxxMapper
@Mapper注解:表示是MyBatis中的Mapper接口
- 程序运行时,框架会自动生成接口的实现类对象(代理对象),并交给Spring的IOC容器管理
- @Select注解:代表的就是select查询,也就是注解对应方法的具体实现内容
使用IDEA自动生成测试类
1、在需要测试的Mapper接口中,右键->Generate->Test

2、书写测试代码
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void getUserInfoAll() {
System.out.println(userInfoMapper.getUserInfoAll());
}
}
测试类上添加了注解@SpringBootTest,在测试类在运行时,就会自动加载Spring的运行环境。我们通过@Autowired这个注解,注入我们要测试的类,就可以开始进行测试了。
运行结果如下:

三、MyBatis的基本操作
上面我们学习了MyBatis的查询操作,接下来我们学习MyBatis的增,删,改操作,在学习这些操作之前,我们先来学习MyBatis日志打印。
3.1 打印日志
在MyBatis当中我们可以借助日志,查看sql语句的执行、执行传递的参数以及执行结果。
在配置文件中进行配置即可
mybatis:
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
重新运行程序,可以看到SQL执行内容,以及传递参数和执行结果。

3.2 参数传递
需求:查询id=2的用户,对应的SQL语句就是:select * from userinfo where id = 2
但是这样的话,只能查找id=2的数据,所以SQL语句中的id值不能写成固定的值,需要变为动态的数值。解决办法:在方法中添加一个参数,将方法中的参数,传给SQL语句,使用#{}的方式获取方法中的参数。
@Select("select * from userinfo where id = #{id}")
List<UserInfo> getUserInfoById(Integer id);
注意:如果mapper接口方法形参只有一个普通类型的参数,#{}里面的属性名可以随便写。建议还是和参数名保持一致。
添加测试用例
@Test
void getUserInfoById() {
System.out.println(userInfoMapper.getUserInfoById(2));
}
运行结果:

也可以通过@Param,设置参数的别名,如果使用@Param设置别名,#{}里面的属性名必须和@Param设置一样
@Select("select * from userinfo where id = #{iid}")
List<UserInfo> getUserInfoById2(@Param("iid") Integer id);
如果#{}里面的属性名必须和@Param设置不一样,我们运行代码。

运行结果:
3.3 增(Insert)
Mapper接口
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into userinfo (username, password, age, gender) " +
"values (#{username}, #{password}, #{age}, #{gender})")
Integer insert(UserInfo userInfo);
直接使用UserInfo对象的属性名来获取参数
测试代码:
@Test
void insert() {
UserInfo userinfo = new UserInfo();
userinfo.setUsername("zzz");
userinfo.setPassword("123");
userinfo.setAge(18);
userinfo.setGender(1);
Integer result = userInfoMapper.insert(userinfo);
System.out.println("result: " + result + ", id" + userinfo.getId());
}
运行结果:

返回主键
Insert语句默认返回的是受影响的行数,但是在有些情况下,数据插入之后,还需要有后续的关联操作,需要获取到新插入数据的id
比如订单系统
当我们下完订单之后,需要通知物流系统,这时就需要拿到订单id
如果想要拿到自增id,需要在Mapper接口上添加一个Options的注解

- useGeneratedKeys:这会令MyBatis使用JDBC的getGeneratedKeys方法来取出数据库内部生成的主键
- keyProperty:指定能够唯一识别对象的属性,MyBatis会使用getGeneratedKeys的返回值或insert语句的selectKey子元素设置它的值
3.4 删(Delete)
Mapper接口
@Delete("delete from userinfo where id = #{id}")
Integer delete(Integer id);
测试用例
@Test
void delete() {
System.out.println(userInfoMapper.delete(10));
}
运行截图

3.5 改(update)
Mapper接口
@Update("update userinfo set username = #{username} where id = #{id}")
Integer update(UserInfo userInfo);
测试用例
@Test
void update() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("李四");
userInfo.setId(9);
System.out.println(userInfoMapper.update(userInfo));
}
运行截图

3.6 查(select)
Mapper接口
@Select("select * from userinfo")
List<UserInfo> getUserInfoAll();
测试用例
@Test
void getUserInfoAll() {
System.out.println(userInfoMapper.getUserInfoAll());
}
运行截图

观察数据库中的字段

从运行结果上可以看出,我们SQL语句中,查询了delete_flag,create_flag,update_time,这几个字字段在数据库中是有赋值的,但是在控制台中显示为空 ,这是为什么呢?
原因分析:
当自动映射查询结果时,MyBatis会获取结果中返回的列名并在Java类中查找相同名字的属性(忽略大小写)。这意味着如果发现了ID列和id属性,MyBatis会将ID列的值赋给id属性。

注意:
MyBatis会根据方法的返回结果进行赋值。
方法用对象Userinfo接收返回结果,MySQL查询出来数据为一条,就会自动赋值给对象。
方法用List<Userinfo>接收返回结果,MySQL查询出来数据为一条或多条时,也会自动赋值给List。
但如果MySQL查询结果返回多条,但是方法返回值使用Userinfo接收,MyBatis执行就会报错。
解决上述为null的办法:
- 起别名
- 结果映射
- 开启驼峰命名
3.6.1 起别名
Mapper接口
@Select("select id, username, password, age, gender, phone, delete_flag as deleteFlag, " +
"create_time as createTime, update_time as updateTime from userinfo")
List<UserInfo> queryAllUser();
测试代码
@Test
void queryAllUser() {
System.out.println(userInfoMapper.queryAllUser());
}
运行截图

3.6.2 结果映射
Mapper接口
@Results(id = "resultMap", value = {
@Result(column = "delete_flag", property = "deleteFlag"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime")
})
@Select("select * from userinfo")
List<UserInfo> queryAllUser2();
测试代码
@Test
void queryAllUser2() {
System.out.println(userInfoMapper.queryAllUser2());
}
运行截图

如果其它SQL,也希望可以复用这个映射关系,可以用上述代码定义的名称resultMap
@Select("select * from userinfo")
@ResultMap(value = "resultMap")
List<UserInfo> queryAllUser3();
3.6.3 开启驼峰命名(推荐)
通常数据库列使用蛇形命名法进行命名(下划线分割各个单词),而Java属性一般遵循驼峰命名法约定。为了在这两种方法之间启动自动映射,需要将mapUnderscoreToCamelCase设置为true。
mybatis:
configuration:
map-underscore-to-camel-case: true
四、MyBatis XML配置文件
MyBatis的开发有两种方式:
- 注解
- XML
上面学习了注解的方式,接下来我们学习XML的方式
使用MyBatis的注解方式,主要是来完成一些简单的增删查改功能,如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件中。
MyBatis XML的方式需要以下两步:
- 配置数据库连接字符串和MyBatis
- 写持久层代码
4.1 配置连接字符串和MyBatis
此步骤需要进行两项设置,数据库连接字符串设置和MyBatis的XML文件配置。
application.yml文件,配置内容如下:
#数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
username: root
password: 111111
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
配置mybatis xml文件路径,在resources/mapper下创建的所有XML文件

4.2 写持久层代码
持久层代码分两部分
- 方法定义interface
- 方法实现:XXX.xml

4.2.1 添加mapper接口
数据库持久层的接口定义:
import com.example.MyBatis.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserinfoXmlMapper {
List<UserInfo> queryAllUser();
}
4.2.2 添加UserinfoXmlMapper.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">
<mapper namespace="com.example.MyBatis.mapper.UserinfoXmlMapper">
</mapper>
创建UserInfoXmlMapper.xml,路径参考yml中的配置

查询所有用户的具体实现:
<?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.model.UserInfo">
select * from userinfo
</select>
</mapper>
以下是对以上标签的说明:
- <mapper>标签:需要指定namespace属性,表示命名空间,值为mapper接口的全限定名,包括全包名.类名
- <select>查询标签:是用来执行数据库的查询操作的:
- id: 是和Interface(接口)中定义的方法名称一样的,表示对接口的具体实现方法
- resultType:是返回的数据类型,也就是开头我们定义的实体类

4.3 增删改查操作
4.3.1 增(Insert)
UserInfoMapper接口:
Integer insertUser(Userinfo userinfo);
UserInfoMapper.xml实现:
<insert id="insertUser">
insert into userinfo (username, password, age, gender, phone) values (#{username},
#{password}, #{age}, #{gender}, #{phone})
</insert>
4.3.2 删(Delete)
UserInfoMapper接口:
Integer deleteUser(Integer id);
UserInfoMapper.xml实现:
<delete id="deleteUser">
delete from userinfo where id = #{id}
</delete>
4.3.3 改(Update)
UserInfoMapper接口:
Integer updateUser(Userinfo userinfo);
UserInfoMapper.xml实现:
<update id="updateUser">
update userinfo set username = #{username} where id = #{id}
</update>
4.3.4 查(Select)
同样的,使用XML的方式进行查询,也存在数据封装的问题,我们把SQL语句进行简单修改,查询更多的字段内容。
<select id="queryAllUser" resultType="com.example.mybatis.model.Userinfo">
select id, username, password, age, gender, phone, delete_flag, create_time, update_time
from userinfo
</select>
运行截图:
结果显示:deleteFlag, createTime, updateTime也没有赋值
解决办法和注解类似:
- 起别名
- 结果映射
- 开启驼峰命名
重点讲下xml来写结果映射
Mapper.xml
<resultMap id="BaseMap" type="com.example.mybatis.model.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" resultType="com.example.mybatis.model.Userinfo">
select id, username, password, age, gender, phone, delete_flag, create_time, update_time
from userinfo
</select>

五、多表查询
多表查询和单表查询类似,只是SQL不同而已
实体类
import lombok.Data;
import java.util.Date;
@Data
public class Userinfo {
private Integer id;
private String username;
private String password;
private Integer age;
private Integer gender;
private String phone;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
Mapper接口
import com.example.mybatis.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ArticleInfoXmlMapper {
ArticleInfo selectArticleAndUserById(Integer id);
}
ArticleInfoXmlMapper.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.ArticleInfoXmlMapper">
<select id="selectArticleAndUserById" resultType="com.example.mybatis.model.ArticleInfo">
select article.*, user.username, user.gender from articleinfo article
left join userinfo user
on article.uid = user.id
where article.id = #{id}
</select>
</mapper>
运行结果截图
六、#{}和${}
MyBatis参数赋值有两种方式,前面使用了#{}进行赋值,接下来我们看下两者的区别
6.1 #{}和${}使用
1、先看Interger类型的参数
@Select("select username, password, age, gender, phone from userinfo where id = #{id}")
Userinfo queryById4(Integer id);
观察打印日志
我们输出的参数并没有在后面拼接,id的值是使用?进行占位,这种SQL我们称之为“预编译SQL”。
我们把#{}改成${}再观察打印的日志:
@Select("select username, password, age, gender, phone from userinfo where id = ${id}")
Userinfo queryById5(Integer id);
观察打印日志
可以看出,这次参数是直接拼接在SQL语句中了。
2.接下来我们再看String类型的参数
@Select("select username, password, age, gender, phone from userinfo where username = #{name}")
Userinfo queryByName(String name);
观察打印日志:
我们把#{}改成${}再观察打印的日志:
@Select("select username, password, age, gender, phone from userinfo where username = ${name}")
Userinfo queryByName2(String name);
观察打印日志:
可以看出,这次的参数依然是直接拼接在SQL语句中了,但是字符串作为参数时,需要添加引号'',使用${}不会拼接引号'',导致程序报错。
修改代码如下:
@Select("select username, password, age, gender, phone from userinfo where username = '${name}'")
Userinfo queryByName2(String name);
再运行观察打印日志:
从上面两个例子可以看出:
- #{}使用的是预编译SQL,通过?占位的方式,提前对SQL进行编译,然后把参数填充到SQL语句中,#{}会根据参数类型,自动拼接引号''
- ${}会直接进行字符替换,一起对SQL进行编译,如果参数为字符串,需要加上引号''
注意:参数为数字类型时,也可以加上,查询结果不变,但是可能会导致索引失效,性能下降。
6.2 #{}和${}区别
#{}和${}的区别就是预编译SQL和即时SQL的区别
简单回顾:
当客户发送一条SQL语句给服务器后,大致流程如下:
- 解析语法和语义,校验SQL语句是否正确
- 优化SQL语句,制定执行计划
- 执行并返回结果
一条SQL如果走上述流程处理,我们称之为Immediate Statements(即时SQL)
6.3 #{}的优势
1、性能更高
绝大多数情况下,某一条SQL语句可能会被反复调用执行,或者每次执行的时候只有个别的值不同(比如select的where子句值不同,update的set子句值不同,insert的values值不同),如果每次都需要经过上面的语法解析,SQL优化、SQL编译等,则效率就明显不行了。
预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译(只是输入的参数不同),省去了解析优化等过程,以此来提高效率。
2、更安全(防止SQL注入)
SQL注入:是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。
由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。
SQL注入代码:' or 1 = '1'
先来看看SQL注入的例子
@Select("select username, password, age, gender, phone from userinfo where username = '${name}'")
List<Userinfo> queryByName3(String name);
测试代码:
正常访问情况:
@Test
void queryByName3() {
System.out.println(userInfoMapper.queryByName3("admin"));
}
结果运行截图

SQL注入场景:
@Test
void queryByName3() {
System.out.println(userInfoMapper.queryByName3("' or 1 = '1"));
}
结果依然被正确查询出来了,其中参数or被当成了SQL语句的一部分
可以看出,查询的数据并不是自己想要的数据,所以用于查询的字段,尽量使用#{}预查询的方式
SQL注入是一种非常常见的数据库攻击手段,SQL注入漏洞也是网络世界中最普遍的漏洞之一。
如果发生在用户登陆的场景中,密码输入为' or 1 = ' 1,就可能完成登陆(不是一定会发生的场景,需要看登陆代码如何写)
6.4 排序功能
从上面的例子中,可以得出结论:${}会有SQL注入的风险,所以我们尽量使用#{}完成查询,既然如此,是不是${}就没有存在的必要性呢?
当然不是
接下来我们看下${}的使用场景
Mapper实现
@Select("select id, username, age, gender, phone, delete_flag, create_time, update_time " +
"from userinfo order by id ${sort}")
List<Userinfo> queryAllUserBySort(String sort);
使用${sort}可以实现排序查询,而使用#{sort}就不能实现排序查询了。
注意:此处sort参数为String类型,但是SQL语句中,排序规则是不需要加引号''的,所以此时的${sort}也不加引号。
我们把${}改成#{}
运行结果:

可以发现,使用#{sort}查询时,desc前后自动给加了引号,导致sql错误
#{}会根据参数类型判断是否拼接引号,如果参数类型为String,就会加上引号。
除此之外,还有表明作为参数时,也只能使用${}
6.5 like查询
like使用#{}报错
@Select("select id, username, age, gender, phone, delete_flag, create_time, update_time " +
"from userinfo where username like '%#{key}%'")
List<Userinfo> queryAllUserByLike(String key);
把#{}改成${}可以正确查出来,但是${}存在SQL注入的问题,所以不能直接使用${}。
解决办法:使用MySQL的内置函数concat()来处理,实现代码如下:
@Select("select id, username, age, gender, phone, delete_flag, create_time, update_time " +
"from userinfo where username like concat('%', #{key}, '%'})")
List<Userinfo> queryAllUserByLike(String key);


1万+

被折叠的 条评论
为什么被折叠?



