前面我们已经学习了Spring、Spring Boot、Spring MVC,现在我们要学习更加重要的MyBatis,查询数据库。
目录
MyBatis
配置MyBatis
创建数据库和表
添加MyBatis框架支持
配置字符连接串和Mybatis
配置MyBatis中的XML路径
xml文件
添加MyBatis代码
用户实体类
添加Mapper接口
添加Service
添加Controller
单元测试
单元测试构建
增、删、改操作
增
删
改
特殊的添加:返回自增id
单表查询
参数占位符 #{} ${}
SQL注入
like查询
多表查询
resultType
resultMap
查询多表关联信息
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
简单来说,MyBatis就是更加简单完成程序和数据库交互的工具,也就是更简单的操作和读取数据库的工具。
对于之前学习的JDBC来说,整个操作数据库的流程非常繁琐,需要手动重复书写很多的操作步骤,这就是我们学习MyBatis的真正原因,因为它足够的方便。
首先我们先把sql语句准备好,创建好个人博客的数据库表:
-- 创建数据库
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';
-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(
vid int primary key,
`title` varchar(250),
`url` varchar(1000),
createtime timestamp default current_timestamp,
updatetime timestamp default current_timestamp,
uid int
)default charset 'utf8mb4';
-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);
-- 文章添加测试数据
insert into articleinfo(title,content,uid)
values('Java','Java正文',1);
-- 添加视频
insert into videoinfo(vid,title,url,uid) values(1,'java title','http://www.baidu.com',1);
使用EditStarters插件快捷添加:
在yml文件中配置好数据库相关的参数。相比于之前JDBC繁琐的操作,如新建DBUtil等等操作,Mybatis直接在配置文件中就可以完成相关参数的设置,并且无需重复操作。
如果使用 mysql-connector-java 是 5.x 之前的,使用的是“ com.mysql.jdbc.Driver ” ,如果是⼤于 5.x 使用的是“ com.mysql.cj.jdbc.Driver ” 。
在resources目录底下新建一个xml文件,并在yml中配置XML路径:
mybatis:
mapper-locations: classpath:mybatis/UserMapper.xml
//这个配置信息在yml文件中
这个文件是mapper的映射文件,所有的sql语句都会在这个里面。前面配置的xml文件路径就是这个文件的路径,确保要一致
用户能够接触到的是控制器层,操作数据库的操作主要是在Mapper层。我们需要给代码分层
在UserEntity类中,定义了用户实体类,并且使用@Data注解
数据持久层的实现,基于方法定义(Interface)和方法实现(xml)。这个Interface可以理解成为之前Servlet中的DAO类,只不过在这里用接口实现的,并且十分简洁。
xml中有所有的sql语句,interface中就是有所有的查询方法,Interface和xml文件是高度关联的,接口中的方法就是在xml文件中。
在接口中,必须要有@Mapper的注释,添加后就可以和XML文件关联起来。
接口中返回一个List,通过getAll方法拿到,那么在XML中就需要完成这个方法。
mapper 中的 namespace 和 mapper 接口需要对应起来。
select 中的id和方法名要一致,并且resultType的值要是需要返回的类的包名。
这样一个查询所有的用户的方法getAll就完成了。
通过@Service把UserService交给IoC容器后,用@Autowired注解把UserMapper接口拿到就可以使用getAll方法。返回的是UserEntity这个类组成的列表。
最后在Controller中配置好了路由映射后,就可以开始在浏览器中访问了。
这样就可以访问到数据库的数据了。相比于Servlet中的JDBC查询,简化了很多内容,这就是框架帮我们简化的内容,框架的好处就在这里体现出来了。
并且采用分层思想,哪一部分出问题了去查看哪一部分的代码就可以了。
单元测试就是针对最小的功能单元编写测试代码。Java程序最小的功能单元是方法,因此,对Java程序进行单元测试就是针对单个Java方法的测试。
在之前的getAll方法中,我们最终是在浏览器中看到的结果,也就是说从实体类到Mapper接口,到Service,到Controller和配置文件中都没有错误,这是我们想要的结果。但是如果调用链很长我们需要测试的仅仅只有一个方法,这个时候就可以使用单元测试来完成。
此时的@Param目的是给需要传入进来的id重命名,在这里不重命名也可以,但是如果有多个参数就一定要命名。
例如我们现在有一个getUserById方法想要测试,在创建测试中勾选这个方法,就可以在Test目录中找到相关的测试方法了。
此时我们新建一个根据Id查询User的方法,只需要在mapper和xml文件中完成相关的代码就可以执行单元测试,无需把所有的调用链都完成。
在xml中,增加使用insert标签,需要指定id,但是不需要指定resultType,因为并不需要返回值,所以不需要指定返回类型。
一个user对象,除了自增主键的id和创建时间,需要用户自己添加的就是用户名和密码了。我们在传入的时候,是直接传入的一个对象,而不是仅仅把对象的两个属性传入进来。这样做可以在之后修改的时候更加简单。
删除操作是根据用户的Id来删除。
使用了@Transactional这个注解,这个注解表示这个测试方法测试过后需要回滚,把数据回滚至之前的状态。
这里实现的方法是根据Id来更改用户名,所以传参数的时候需要传入两个值,一个是Id,一个是name。
在刚刚演示的增删改操作中,方法返回的都是int类型,返回的都是受影响的行数。如果想要返回的是自增的id,也可以做到:
和之前的add方法,基本上一样,不过多了几个参数:
- useGeneratedKeys:是否拿到表的自增Id(默认值是false)
- keyProperty:把刚刚拿来的自增Id放到哪一个属性中(默认是未设置unset)
也就是说,通过这两个参数的设置,拿到自增的ID并且赋值到user的Id中,此时可以通过get方法拿到Id:
在前面的xml文件中,我们使用了两种传参数的占位符,这两种方法都可以。
区别在于:
- #{}:预编译处理:在处理时,会将SQL中的 #{} 替换成?,然后再使用PrepareStatement的set方法来赋值。
- ${}:字符直接替换:在处理时,把 ${} 的值直接替换成变量的值。
先说结论:两种方式缺一不可。但是尽量使用#{},因为${}会出现SQL注入这样的问题。
那为什么不完全用#{}呢?因为在某些情况只能是${}才能完成。
例如在按照某种顺序查询的时候,如果使用#{}的方式,会出现无法查询的情况:
select * from userinfo order by id ${sort}
select * from userinfo order by id #{sort}
例如使用${sort}时,可以正常的完成查询。而使用#{sort}时,就因为传递的值为String拼接而成,导致拼接的时候会加上单引号,导致sql错误。
当我们需要查询数据库,此时被人恶意拼接某些字符串,就会导致数据泄露。
select * from userinfo where username='${name}' and password='${password}'
正常输入的用户名为zhangsan,密码为123,此时数据库的查询语句就会变成:
select * from userinfo where username='zhangsan' and password='123'
但是如果我们恶意拼接字符串 ' or 1='1
这样,这个sql语句就变成了:
这样不管前面的用户名和密码输入的是什么,最后的 1 = '1' 总是成立的,这样岂不是就算没有正确的密码也能返回用户信息了?
所以,使用 ${} 是非常危险的行为!需要仔细仔细再仔细!
like查询是模糊查询:
select * from userinfo where username like '%zhang%' ;
正常情况下,使用like查询可以通过单引号内加%的形式来匹配,前后都有百分号则表示只要出现了zhang这个关键字就能够查询到。
但是由于在mybatis中需要使用参数占位符,并且使用${}会出现sql注入的现象,所以只能使用#{}来代表参数,这个时候代码就会变成这样:
select * from userinfo where username like '%#{username}%' ;
此时的username为zhang。但是因为zhang为字符串,最终在mysql中执行的语句就变成了:
select * from userinfo where username like '%'zhang'%' ;
此时就会出现zhang这个字符串多出来了''一串单引号,拼接到sql语句中就报错了。
所以为了确保安全,又要保证模糊查询生效,解决办法就是使用mysql内置的concat函数来完成,
concat可以拼接多个字符串,如:
select * from userinfo where username like concat('%',#{username},'%');
这样就可以正常的使用模糊查询了。
增、删、改操作,在xml中是无需返回类型的。
而查询操作需要返回一个resultType类型,这个类型对应着实体类。
也就是说,实体类中的字段名要和数据库中的名字匹配才能够查询到具体的值。
此时使用resultType可以正常查询到相关的值。
resultType的好处是方便,只需要在查询操作时返回一个对应的类就可以了。但是一旦实体类是的成员名和数据库中的不匹配,那么就会查询不到。
resultMap的基础格式:
其中,第一行的id指的是这个map的id,也就是和后面匹配的时候的id:
id column指的是主键映射,result指的是普通的字段和属性映射。
此时就算数据库中的字段和实体类中的字段名字不一样,在映射过后也可以查询到。
我们需要查询文章表的信息,文章表中有这样一些信息,我们创建好实体类:
我们还需要用户表中的用户名,也就是需要同时查询两张表的信息。
我们在新建一个实体类ArticleInfoVO,在这个实体类中,加入了需要查询的用户名,并且继承ArticleInfo类。这样只需要在接口和xml中定义新的这个实体类就能够拿到两张表的信息。
此时我们将ArticleMapper和xml对应上:
并且把相关的映射对象修改好,SQL语句:
select a.*,username from articleinfo a
left join userinfo u on u.id=a.uid
where a.id=#{id}
把articleinfo重命名成a,userinfo重命名成u,join on的条件为 id = uid ,总语句传入的参数为articleinfo表的id。
但还会发现一个问题,最终结果并不会打印username的方法,这是因为Lombok在重写toString的时候,只会重写打印当前的方法,我们还需要把toString再生成一遍,并且选中重写当前方法和父类的方法。