目录
今日良言:与其抱怨于黑暗,不如提灯向前行
一、初识MyBatis
1.MyBatis定义
2.为什么要学习MyBatis
3.MyBatis的创建
二、MyBatis的相关操作
1.增删改查操作
2.动态SQL使用
MyBatis 是⼀款优秀的持久层框架,它⽀持⾃定义 SQL、存储过程以及⾼级映射。MyBatis 去除了⼏ 乎所有的 JDBC 代码以及设置参数和获取结果集的⼯作。MyBatis 可以通过简单的 XML 或注解来配置 和映射原始类型、接⼝和 Java POJO(Plain Old Java Objects,普通⽼式 Java 对象)为数据库中的记录。
简单来说 MyBatis 是更简单完成程序和数据库交互的⼯具,也就是更简单的操作和读取数据库⼯具。
1). 后端程序2). 数据库而这两个重要的组成部分要通讯,就要依靠数据库连接⼯具,那数据库连接⼯具有哪些?比如之前我们学习的 JDBC,还有今天我们将要介绍的 MyBatis,那已经有了 JDBC 了,为什么还要学习 MyBatis?这是因为 JDBC 的操作太繁琐了,回顾⼀下 JDBC 的操作流程:1). 创建数据库连接池 DataSource
2). 通过 DataSource 获取数据库连接 Connection
3). 编写要执⾏带 ? 占位符的 SQL 语句
4). 通过 Connection 及 SQL 创建操作命令对象 Statement
5). 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值
6). 使⽤ Statement 执⾏ SQL 语句
7). 查询操作:返回结果集 ResultSet,更新操作:返回更新的数量
8). 处理结果集
9). 释放资源
因此可以通过MyBatis简化上述步骤,使得操作数据库更加简单。
学习MyBatis的步骤大致可以分为两步:
● 配置 MyBatis 开发环境;
● 使⽤ MyBatis 模式和语法操作数据库。
创建 MyBatis 之前,我们先来看⼀下 MyBatis 在整个框架中的定位,框架交互流程图:
MyBatis 也是⼀个 ORM 框架,ORM(Object Relational Mapping),即对象关系映射。
接下来添加MyBatis 相关依赖。
如果是旧老项目(原有项目基础上添加MyBatis的相关依赖),使⽤EditStarters插件进行快速添加。
安装EditStarters插件。
在pom.xml 配置文件中进行如下操作:
由于MyBatis 是一个中介,是连接数据库和程序的中介,所以需要选择对应的数据库,这里选择MySQL Driver。
然后点击OK即可添加成功、。
上述是原有项目添加 MyBatis的相关依赖步骤。
新项目添加MyBatis依赖流程如下:
接下来由于相关操作都是和MySQL数据库相关联,这里使用如下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);
接下来,需要配置连接字符串和MyBatis。
# 配置数据库的连接字符串
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#设置MyBatis的配置
mybatis.mapper-locations=classpath:/mybatis/*Mapper.xml
这里的classpath 表示当前根路径,mybatis 表示所有mybatis的配置都会放在这个文件夹下,*Mapper.xml 表示所有和MyBatis 相关的 xml文件都叫做*Mapper.xml,比如和用户相关的叫做 UserMapper.xml 和 文章相关的叫做 ArticleMapper.xml.
如下图:
MyBatis 模式开发由两部分组成:
1.interface :让其它层可以注入使用的接口
添加实体类,代码如下:
package com.example.demo.entity; import lombok.Data; import java.time.LocalDateTime; /** * @author 26568 * @date 2023-06-24 16:34 */ @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;// 表示状态,是1就是正常用户 }
这里的@Data 注解来自lombok 插件,针对lombok的学习可以借鉴如下这篇博客:
IDEA从零到精通(24)之lombok插件的安装与使用_编程界小明哥的博客-CSDN博客
上述实体类中的属性需要和数据库表中的对应
接下来,以查询实体类(用户)操作为例,继续完善代码:
2.mybatis: xml—> 具体实现sql(是上面interface的“实现”)
创建UserMapper.xml文件,实现具体sql.
UserMapper.xml 文件的相关配置代码如下:
需要注意,这里的路径要和一个具体的接口对应:
然后继续在UserMapper.xml 文件中完善sql代码:
注:
:是⽤来执⾏数据库的查询操作的:id: 是和 Interface(接⼝)中定义的⽅法名称⼀样的,表示对接⼝的具体实现⽅法。resultType :是返回的数据类型,也就是开头我们定义的实体类当sql 代码完善以后,实现服务层(Service)和控制层(Controller)代码。服务层(Service)代码:package com.example.demo.service; import com.example.demo.entity.UserInfo; import com.example.demo.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * @author 26568 * @date 2023-06-24 19:54 */ @Service // 将这个类存放到 Spring 中 public class UserService { @Autowired private UserMapper userMapper;// 这是接口 public List
getAll() { return userMapper.getAll(); } } 控制层(Controller)代码:
package com.example.demo.controlleer; import com.example.demo.entity.UserInfo; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * @author 26568 * @date 2023-06-24 19:53 */ @RequestMapping("/user") @RestController public class UserController { @Autowired private UserService userService; @RequestMapping("/getall") public List
getAll() { return userService.getAll(); } } 所有目录结构如下图所示:
当启动项目后,输入网址:127.0.0.1:8080/user/getall 得到如下结果:
查询userinfo表中数据:
此时,当继续往userinfo 表中插入一条数据以后,查询会再次显示:
以上就是一个完整的MyBatis模式开发流程。
MyBatis模式开发的具体业务流程如下图:
在进行相关操作之前,先来介绍一下单元测试,
单元测试(unit testing),是指对软件中的最⼩可测试单元进⾏检查和验证的过程就叫单元测试。单元测试是开发者编写的⼀⼩段代码,⽤于检验被测代码的⼀个很⼩的、很明确的(代码)功能是否正确。执⾏单元测试就是为了证明某段代码的执⾏结果是否符合我们的预期。如果测试结果符合我们的预期,称之为测试通过,否则就是测试未通过(或者叫测试失败)
单元测试的好处:
1.)可以非常简单、直观、快速的测试某⼀个功能是否正确。2)、使⽤单元测试可以帮我们在打包的时候,发现⼀些问题,因为在打包之前,所以的单元测试必须通 过,否则不能打包成功。3)、使⽤单元测试,在测试功能的时候,可以不污染连接的数据库,也就是可以不对数据库进行任何改变的情况下,测试功能。
点击OK以后会生成如下类:
package com.example.demo.mapper;
import com.example.demo.entity.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest // 添加这个注解 表示当前单元测试的类是运行在 Spring Boot环境中的
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void getAll() {
List list = userMapper.getAll();
for (UserInfo userInfo:list) {
System.out.println(userInfo);
}
}
}
完善生成的单元测试的类的代码:
package com.example.demo.mapper;
import com.example.demo.entity.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest // 添加这个注解 表示当前单元测试的类是运行在 Spring Boot环境中的
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void getAll() {
List list = userMapper.getAll();
for (UserInfo userInfo:list) {
System.out.println(userInfo);
}
}
}
点击运行单元测试,查看打印结果:
接下来继续进行MyBatis的相关操作。
查询操作:
通过id查询用户:
UserMapper 接口的代码如下:
@Mapper
public interface UserMapper {
List getAll();
// 根据id查询用户
UserInfo getById(@Param("id")Integer id);
}
UserMapper.xml 新增sql语句如下:
此时,再次使用单元测试,在UserMapper中右键......(和上面流程一样),只是此处会报如图所示的错误:
点击OK即可,这里的意思是当前类已经创建了单元测试类,是否进行修改。
单元测试代码如图所示:
点击进行运行,查看打印结果:
注:
这两处名字要保持一致,Use
MyBatis 获取动态参数有两种实现:
1)、${ }:直接替换。
直接替换:是MyBatis 在处理 ${} 时,就是把 ${} 替换成变量的值。
2)、#{ }: 预编译处理。
预编译处理是指:MyBatis 在处理#{}时,会将 SQL 中的 #{} 替换为?号,使用PreparedStatement 的 set ⽅法来赋值。在application.properties文件添加如下配置可以观察到上述两种获取参数的方法所对应的最终 sql 执行语句:# 打印MyBatis 执行SQL mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl # 配置日志的级别 logging.level.com.example.demo=debug
此时,再次启动单元测试,观察直接替换的SQL执行语句:
使用#{} 观察打印结果:
使用${ } 的方式可能会造成SQL注入问题:
简单的Integer 传参不会有问题,通过如下根据名称查询用户对象会造成SQL注入问题:
UserMapper 接口中新增代码:
// 根据名称查询对象 UserInfo getUserByUserName(@Param("username")String username);
UserMapper.xml 新增代码:
使用单元测试:
@Test void getUserByUserName() { UserInfo userInfo = userMapper.getUserByUserName("lisi"); System.out.println(userInfo); }
运行单元测试查看结果:
使用#{ } 运行正常,此时,使用${} 再次查看打印结果:
发生错误。
正常的SQL语句,字符串应该加单引号,但是${ } 是直接替换,没有单引号,所以报错。想要不报错,手动加上单引号可以解决,但是这会造成SQL注入问题。
SQL注入安全一般发生在登录,以登录为例,来解释SQL注入:
UserMapper 接口中的新增代码如下:
// 登录 UserInfo login(UserInfo userInfo);
这里传对象就不需要使用@Param注解了。
UserMapper.xml 新增代码如下:
这里虽然传入的是对象,但是由于框架会帮助我们完成自动映射,所以可以直接使用用户属性。
单元测试代码如下:
@Test void login() { UserInfo userInfo = new UserInfo(); userInfo.setPassword("admin"); userInfo.setUsername("admin"); UserInfo user = userMapper.login(userInfo); System.out.println(user); }
运行单元测试,查看结果,运行正确:
当密码或者结果输入错误,运行结果为null:
发送SQL注入的代码如下:
运行单元测试查看结果,由于数据库中只有一个用户admin:
上述密码输入错误,应该无法查到结果,但是实际输出却是得到了正确的结果:
上述就可以认为是发生了SQL注入。
直接替换的SQL语句如下:
运行单元测试,查看结果:
综上,⽤于查询的字段,尽量使用 #{} 预查询的方式 。
使用${ } 优点:
使用${ } 可以实现排序查询,而#{ } 无法实现排序查询。
UserMapper 接口中的新增代码如下:
// 排序 List
getAll2(@Param("ord")String ord); UserMapper.xml 新增代码如下:
单元测试代码如下(观察SQL语句执行是否正确):
@Test void getAll2() { List
list = userMapper.getAll2("desc"); System.out.println(list.size()); } 运行单元测试查看结果:
结果正确。
将${ } 替换成 #{ },运行单元测试查看打印结果:
like 查询 使用 #{} 会出错。
UserMapper 接口中的新增代码如下:
// 使用用户名进行模糊查询 List
getListByName(@Param("username")String username); UserMapper.xml 新增代码如下:
单元测试代码如下:
@Test void getListByName() { String username = "ad"; List
list = userMapper.getListByName(username); System.out.println(list.size()); } 运行单元测试,查看执行结果:
发生错误。
不能直接使⽤ ${},可以考虑使⽤ mysql 的内置函数 concat() 来纠正上述错误。
concat 的作用是将传入的参数拼接起来。
UserMapper.xml 代码更新如下:
此时再次运行单元测试,执行结果正确:
返回类型:resultType >:
绝⼤数查询场景可以使用 resultType 进行返回,它的优点是使用方便,直接定义到某个实体类即可。
但是,有的场景下,就不能使用resultType 进行返回了。如下:
数据库的密码字段是password,但是创建的实体类属性是pwd:
此时,运行上述根据id查询用户的单元测试,查看执行结果:
会发现,由于password 和 pwd 不相同,所以数据库查询到的数据无法与实体类相对应的属性进行映射。
针对这种情况,就需要使用字典映射resultMap
resultMap 的使用场景有两个:
1)、字段名称和程序中的属性名不同的情况,可使⽤ resultMap 配置映射;2)、一对一和一对多关系可以使用resultMap 映射并查询数据
上述就是resultMap的第一个使用场景,使用步骤:
首先需要在xml文件中定义resultMap:
此时需要修改 根据id查询用户的xml 代码:
再次运行单元测试,查看执行结果:
另外一种解决方案就是使用使用重命名,给数据库字段password 起别名为 pwd:
再次运行单元测试,查看执行结果:
以上所有操作均是单表查询。接下来学习一下多表查询。
多表查询
首先创建一个实体类 ArticleInfo,代码如下:
@Data public class ArticleInfo { private Integer id; private String title; private String content; private LocalDateTime createtime; private LocalDateTime updatetime; private Integer uid; private Integer rcount; private Integer state; }
这里需要再创建一个实体类ArticleInfoVO,因为后续多表操作返回结果可能有用户名,直接在实体类ArticleInfo 中添加字段不太好,所以需要创建ArticleInfoVO ,代码如下:
package com.example.demo.entity.vo; import com.example.demo.entity.ArticleInfo; import lombok.Data; /** * @author 26568 * @date 2023-06-26 13:56 */ @Data public class ArticleInfoVO extends ArticleInfo { private String username; @Override public String toString() { return "ArticleInfoVO{" + "username='" + username + '\'' + "} " + super.toString(); } }
创建ArticleMapper接口:
package com.example.demo.mapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ArticleMapper { }
在mybatis包下创建ArticleMapper.xml 文件:
接下来执行如下操作:根据文章id查询文章详情。
UserMapper接口 新增代码如下:
@Mapper public interface ArticleMapper { // 查询文章详情 ArticleInfoVO getDetail(@Param("id")Integer id); }
UserMapper.xml 新增代码如下:
创建在ArticleMapper 接口中,进行创建单元测试步骤......
getDatail方法的单元测试的代码如下:
package com.example.demo.mapper; import com.example.demo.entity.vo.ArticleInfoVO; 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 ArticleMapperTest { @Autowired private ArticleMapper articleMapper; @Test void getDetail() { ArticleInfoVO articleInfoVO = articleMapper.getDetail(1); System.out.println(articleInfoVO); } }
执行单元测试,查看打印结果:
接下来,进行查询一个用户的多篇文章操作:
UserMapper 接口新增代码如下:
// 查询用户的所有文章 List
getListByUid(@Param("uid")Integer uid); UserMapper.xml 新增代码如下:
新增getListByUid方法的单元测试,代码如下:
@Test void getListByUid() { Integer uid = 1; List
list = articleMapper.getListByUid(uid); System.out.println(list.size()); } 运行单元测试,查看执行结果:
修改操作:
修改用户密码:
UserMapper 接口新增代码:
// 修改密码
int update(@Param("id")Integer id,
@Param("password")String password,
@Param("newPassword")String newPassword);
UserMapper.xml 新增代码如下:
update userinfo set password = #{newPassword} where id = #{id} and password = #{password}
单元测试代码如下:
@Test
void update() {
int result = userMapper.update(1,"admin","123456");
System.out.println("修改:"+result);
}
运行单元测试,查看结果:
查看数据库用户密码,发现被修改:
前面介绍说使用单元测试的优点之一是不会污染连接的数据库,但是这里显然是污染了数据库,这不是自相矛盾吗?
其实不然,默认情况下,单元测试会污染连接的数据库,但是,当为单元测试添加@Transactional (事务)注解以后就不会污染数据库,关于事务相关介绍,博主之前的博客有过详细介绍:
(2条消息) Spring 事务和事务传播机制_程序猿小马的博客-CSDN博客
此时,运行单元测试,查看结果:
查看数据库:
会发现密码并没有被修改。
删除操作:
删除用户:
UserMapper 接口新增代码:
// 删除用户
int delete(@Param("id")Integer id);
UserMapper.xml 新增代码如下:
delete from userinfo where id = #{id}
单元测试代码如下:
@Transactional
@Test
void delete() {
int result = userMapper.delete(1);
System.out.println("删除:"+result);
}
运行单元测试,查看结果:
删除操作执行成功,但是由于事务的存在,发生回滚,不会污染连接的数据库。
添加操作:
添加用户:
添加用户传入的参数是对象,如果传入的是参数,后续发生修改的话,整个调用链都需要进行修改。
UserMapper 接口新增代码:
// 添加用户
int addUser(UserInfo userInfo);
UserMapper.xml 新增代码如下:
insert into userinfo (username,password) values(#{username},#{password})
单元测试代码如下:
@Test
void addUser() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("张三");
userInfo.setPassword("1111111");
int result = userMapper.addUser(userInfo);
System.out.println("添加:"+result);
}
运行单元测试,查看执行结果和数据库数据:
上述添加用户操作的返回结果只是一个受影响的行数,如果想要返回添加用户的id该如何操作?
UserMapper 接口新增代码:
// 添加用户返回id
int addUser1(UserInfo userInfo);
UserMapper.xml 新增代码:
insert into userinfo (username,password) values(#{username},#{password})
这里的 useGeneratedKeys="true" 意思就是返回添加成功的用户的id。
keyProperty="id" 的意思是返回添加成功的用户的id放在哪个"id" 字段中。
单元测试的代码:
@Test
void addUser1() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("李四");
userInfo.setPassword("22222");
int result = userMapper.addUser1(userInfo);
System.out.println("添加成功的用户的id是:"+userInfo.getId());
}
动态 sql 是Mybatis的强⼤特性之⼀,能够完成不同条件下不同的 sql 拼接。
动态sql 就是为了能在sql语句中进行逻辑判断。
介绍一下动态SQL中常用的几个标签:
1)、
这个时候就需要使⽤动态标签
// 添加用户(包含非必填字段)
int addUser2(UserInfo userInfo);
UserMapper.xml 新增代码如下:
insert into userinfo (username,password
,photo
) values (#{username,#{password}
,#{photo}
)
新增单元测试代码如下:
@Transactional
@Test
void addUser2() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("张三");
userInfo.setPassword("11111");
int ret = userMapper.addUser2(userInfo);
System.out.println("添加:"+ret);
}
运行单元测试,查看执行结果:
传入两个字段:
修改单元测试代码,传入photo 属性,运行并查看结果:
注:test 中的photo 是属性,不是数据库字段。
2)、
prefix:表示整个语句块,以prefix的值作为前缀suffix:表示整个语句块,以suffix的值作为后缀prefixOverrides:表示整个语句块要去除掉的前缀
suffixOverrides:表示整个语句块要去除掉的后缀
还是以添加用户为例:这里假设 username,password,photo 都是非必传字段
UserMapper 新增代码如下:
// 添加用户
int addUser3(UserInfo userInfo);
UserMapper.xml 新增代码如下:
insert into userinfo
username,
password,
photo
values
#{username},
#{pwd},
#{photo}
新增单元测试的代码如下:
@Transactional
@Test
void addUser3() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("张三");
userInfo.setPwd("111111");
int ret = userMapper.addUser3(userInfo);
System.out.println("添加:"+ret);
}
运行单元测试,查看执行结果:
当传入photo属性:
3)、
传入的用户对象,根据属性做 where 条件查询,⽤户对象中属性不为 null 的,都为查询条件。使用where标签还有一个好处,可以去掉前缀(and 或者 or)。
以查询文章为例:
ArticleMapper 接口新增代码如下:
List getListByIdOrTitle(@Param("id")Integer id,
@Param("title")String title);
ArticleMapper.xml 文件新增代码如下:
新增单元测试的代码如下:
@Test
void getListByIdOrTitle() {
List list = articleMapper.getListByIdOrTitle(null,null);
System.out.println(list.size());
}
运行单元测试,查看执行结果:
传入title:
再次运行单元测试,查看结果:
4)、
根据传入的用户对象属性来更新⽤户数据,可以使⽤
最后一个逗号。
set 标签的用法和 where标签比较相似,这里就不做过多介绍。
5)、
对集合进行遍历时可以使用该标签。
collection:绑定⽅法参数中的集合,如 List,Set,Map或数组对象。item:遍历时的每⼀个对象。open:语句块开头的字符串。close:语句块结束的字符串。separator:每次遍历之间间隔的字符串 。
使用foreach标签最常用的场景是批量操作。
以批量删除文章为例:
ArticleMapper 接口新增代码如下:
// 根据id删除文章集合
int delByList(List idList);
ArticleMapper.xml 新增代码如下:
delete from articleinfo
where id in
#{aid}
新增单元测试代码:
@Test
void delByList() {
List idList = new ArrayList<>();
idList.add(1);
idList.add(2);
int ret = articleMapper.delByList(idList);
}
运行单元测试,查看执行结果:
以上就是关于MyBatis 的所有内容。