本节目标
- 使用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入门
2.1 准备工作
2.1.1 创建工程
2.1.2 数据准备
2.2 配置数据库连接字符串
2.3 写持久层代码
三、MyBatis的基本操作
3.1 打印日志
3.2 参数传递
3.3 增(Insert)
3.4 删(Delete)
3.5 改(update)
3.6 查(select)
3.6.1 起别名
3.6.2 结果映射
3.6.3 开启驼峰命名(推荐)
四、MyBatis XML配置文件
4.1 配置连接字符串和MyBatis
4.2 写持久层代码
4.2.1 添加mapper接口
4.2.2 添加UserinfoXmlMapper.xml
4.3 增删改查操作
4.3.1 增(Insert)
4.3.2 删(Delete)
4.3.3 改(Update)
4.3.4 查(Select)
五、多表查询
六、#{}和${}
6.1 #{}和${}使用
6.2 #{}和${}区别
6.3 #{}的优势
6.4 排序功能
6.5 like查询
一、什么是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);