点进来你就是我的人了
博主主页:戳一戳,欢迎大佬指点!欢迎志同道合的朋友一起加油喔
目录
1. 什么是 MyBatis
1.1 为什么要学MaBatis?
2. 如何学习 MyBatis
3. 搭建 MyBatis 开发环境
4. 使用 MyBatis 模式和语法操作数据库
4.1 MyBatis 的组成
4.2 MyBatis 实现查询功能
第一步: 创建实体类
第二步: 创建一个接口
第三步: 创建与接口对应的 xml 文件
第四步: 在 xml 中写 SQL 语句
第五步: 创建 Service 类:
第六步: 创建 Controller 类:
图解Mybatis执行流程:编辑
第七步: 将接口中对应的方法生成 Spring Boot 单元测试
4.3 根据指定参数查询数据库 (单表查询)
4.3.1 ${xxx}获取动态参数
4.3.2 #{xxx}获取动态参数
4.3.3 #{xxx}与${xxx}获取字符串类型数据
4.3.4 sql注入问题
4.3.5 模糊查询like
4.3.6 #{}与${}区别总结
4.4 增、删、改操作
1. 修改操作 (update 标签) [ @Transactional 注解防止数据库污染]
2.删除操作 ( delete 标签)
3. 添加操作 ( insert 标签)
4.5 多表查询的进阶知识
4.5.1 查询的返回类型: resultMap
4.5.2 多表查询 (一对一)
多表查询示例代码: (一对一)
4.5.3 多表查询(一对多): 重命名方式实现
5.动态 SQL 的使用
5.1 if 标签
5.2 trim 标签
5.3 标签
mybatis中多个都是非必传参数的解决方案: (配置xml文件实现上面接口)
解决方案1: where 1=1
解决方案2: trim标签的方式
解决方案3: 标签
5.4 set 标签
5.5 foreach 标签
MyBatis 是一款神级持久层框架, 它支持自定义 SQL , 存储过程以及高级映射. 简单来说, MyBatis 就是基于 JDBC 做到更简单的完成程序与数据库交互的高级框架. -- MyBatis 3 | 简介
MyBatis 也是一个 ORM 框架, ORM (Object Relational Mapping), 即对象关系映射, 在面向对象编程语言中,将关系型数据库中的数据与对象建立起映射关系,进而自动的完成数据与对象的互相转换. (JDBC 更像是面向过程的编程思想, MyBatis 更像是面向对象的编程思想)
对于后端开发来说,完整的程序由以下两部分组成:
后端程序与数据库之间的通讯,就得依靠数据库连接工具,像之前学习的JDBC,但是因为JDBC使用起来太繁琐了,简单回顾使用JDBC操作数据库的步骤:
所以呢,使用MaBatis简化上述操作,方便快速的操作数据库
学习 MyBatis 可以分为两个大的方向学习:
第一步: 创建一个 Spring Boot 项目, 并添加 MyBatis 框架支持
此时你的程序是不能正常运行的.
(因为只是告诉mybatis需要连接数据库,但是没有mybatis告诉要连接的数据库的具体信息)
第二步: 配置数据库的连接信息
在 resource 目录下创建一个 applicaiton.yml 文件 (根据自己喜好, properties 和 yml 都行)
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8&&useSSL=false
username: root
password: 1111
driver-class-name: com.mysql.cj.jdbc.Driver # 这里表示 MySQL 驱动名称
注意事项: 如果使⽤ MySQL 是 5.x 之前的使⽤的是“com.mysql.jdbc.Driver”,如果是⼤于 5.x 使⽤的是“com.mysql.cj.jdbc.Driver”;
第三步: 配置 MyBatis 的 XML 文件存放位置和命名规则
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
yml 配置文件中的 myBatis/**Mapper.xml 的意思是 :
- 在 resource 下创建一个 myBatis 目录, 然后所有的 xml 都存放在这个目录下, xml 里面写的就是 SQL 语句. 当然 myBatis 是我自己取的名字, 这个可以随意命名.
- **Mapper.xml 就表示在 mybatis 目录下创建的 xml 文件的命名要以 Mapper.xml 结尾, 例如 UserMapper.xml
配置 mapper-location 的作用是需要搭配一个注解来说的, 后面会提到.
此时我们的项目就可以正常运行起来了
同时配置MyBatis中sql执行打印
#设置mybatis
mybatis:
mapper-locations: classpath:mybaits/**Mapper.xml
#配置mybatis MyBatis执行的每个SQL语句都会打印到控制台。
configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# sql打印日志的级别为debug,但是spring boot默认的打印级别为info
# 设置com.example.demo包下的打印级别为debug (只要高于这个级别的日志消息都会被记录)
logging:
level:
com.example.demo: debug
MyBatis 由接口和 xml 组成, 一个类会有一个接口和一个对应实现相应操作的 xml 文件.
- 接口中是增、删、改、查等方法的声明, 存在的目的就是给别人调用的.
- xml 中存放的就是增删改查的 SQL 语句, xml 中的每条 SQL 和接口中的方法一一对应. (sql 如果存写在类中, 需要用双引号引起来, 有时候有变量还需要处理拼接, 太麻烦, 所以存放在 xml 中)
大致流程图:
先准备数据 - 建库建表
-- 创建数据库
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);
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private Date createtime;
private Date updatetime;
private int state;
}
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper; // MyBatis 以前叫做 ibatis
import java.util.List;
@Mapper // 此注解必不可少, 该注解会将 接口 和 xml 对应起来
public interface UserMapper {
// 查询
List getAll();
}
【重点】接口上必须要添加 @Mapper 注解, 此注解的作用:
MyBatis 的接口和 xml 是如何关联起来的 ? 【重点理解】
1. xml 的创建和命名规则前面在 yml 中已经配置了 (根据自己的配置) , 必须在创建在 myBatis 文件夹下, 命名要以 Mapper.xml 结尾. 当程序运行的时候, MyBatis 会去找它自己的注解 @Mapper, 然后再根据 yml 文件中配置的路由再去匹配当前接口所有对应的 xml 文件中的具体实现.从而将接口和 xml 关联起来. 这一切的一切都是 MyBatis 去做的, 但是我们必须要遵守规范!!
2. 上面 MyBatis 中的 xml 模板是通用的, 唯独要自己写的地方就是 namespace, namespace 里需要配置当前 xml 要实现的接口的路径, 配置好 xml 时, 此时接口和 xml 就关联上了.
1. 查询使用
- id 属性上的 getAll 对应接口中的方法名 【千万不敢写错】
- resultType 表示查询返回对象的类型 (包名 + 实体类类名) 【千万不敢漏写】
2.
SQL 语句是如何将查询结果带回的呢 ? 【重点理解】
我们的查询语句首先会从数据库中查到数据, 然后再根据 resultType 去找对应的实体类, 然后通过 getAll() 方法, 将查到的每条数据的每个字段设置到实体类的每个属性中, 此时数据库中每条记录的值就能够在对应实体类中的属性上体现出来了, 所以 MyBatis 做的事情就是封装了 JDBC 的代码, 然后将数据保存到 List
里面了, 此时我们在项目中只要实现注入了, 就能够在任何地方拿到查询的结果了. 所以我们一定要保证实体类属性名和数据库中字段名保持一致!! (如果不一致, 也有解决方法, 下面会详细讲到)
在 UserService 类中,我们采取 " 属性注入 " 的方式,将 " userMapper " 对象存入进来,之后就能使用接口中的方法了。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List getAll() {
return userMapper.getAll();
}
}
备注: 属性注入不仅可以存入类,也可以存入接口。
在 UserController 类中,我们依然采取 " 属性注入 " 的方式,将 " userService " 对象存入进来,之后就能使用类中的方法了。
在这里,我们明确了给前端返回的是一个数据,也明确了路由、传入参数。
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/getall")
public List getAll() {
return userService.getAll();
}
}
注意事项
这里需要思考,我们为什么不使用 " Controller 类 " 直接调用 " Mapper 接口 ",而是让 " Service " 在中间插了一杠子?
- 首先,这是一个 Web 项目查询数据库的标准步骤。
- 其次,这样分层地拆分程序,能够更好地解耦合,也能够更好地为后端分工。
- 与前端交互的事情,就让 " Controller " 层去做,交互出了问题,直接看这一层。
- 调用接口的事情,就让 " Service " 层去做,出了问题,直接定位这一层。
- 查询数据库的事情,就让 " Mapper " 接口去做…
- 在Java中,接口确实不能被直接实例化。然而,MyBatis和Spring等框架可以动态生成接口的实现类,并创建这些实现类的实例。当你通过依赖注入获取一个接口的实例时,你实际上是获取了这个动态生成的实现类的实例。
- 在MyBatis中,这是通过使用Java的动态代理实现的。当你调用
SqlSession.getMapper(UserMapper.class)
或者在Spring中使用@Autowired
注入UserMapper
时,MyBatis会创建一个UserMapper
的代理对象。当你调用这个对象上的方法时,代理对象会将调用转发给MyBatis,然后MyBatis执行相应的SQL语句。
【单元测试】
单元测试的好处: 以最小成本, 花最短时间去验证我们的程序是否正确.
如何使用单元测试验证程序功能:
- 这一步是不需要我们操作的, 因为我们在创建 Spring Boot 项目的时候, 默认会给每个项目都内置单元测试的框架.
- 在接口中右击选择 Generate -> Test, 然后选中要单元测试的方法, 然后点击 ok, 就会生成如下单元测试类:
这时候, 我们还需要做一些事情, 才能测试查询功能是否正确:
- 对单元测试的类加上 @SpringBootTest 注解, 表示当前测试的上下文环境为 Spring Boot.
- 注入需要测试的接口, 这里需要测试 UserMapper (属性注入, Setter 注入, 构造方法注入)
- 在测试方法中使用接口中的方法, 去实现业务逻辑.
- 点击测试方法左边的三角形, 运行程序, 验证测试结果.
测试结果:
在单元测试中使用@Transactional注解可以防止污染数据库
使用这个注解后,在进行单元测试的时候,是不会修改数据库里的内容的,因此当我们进行增删查改的时候,可以先这样进行测试看到底有没有问题,再进行后续操作。这个在下面的操作中也会进行演示。
上述的示例中,查询所有用户的信息,那此时如果想获得id为1的用户的信息该怎么操作?那就要在接口的方法中传递参数了
@Param与@RequestParam用法差不多,也是获取并赋值的意思,并且要求必传,也就是要必须能拿到@Param中的键对应的值
使用${xxx}
表示传递的参数,此时xxx必须与@Param中的键对应,如果接口方法中没有用@Param修饰形参,那xxx必须与形参名称对应
使用postman测试结果如下:
执行单元测试类看打印结果: (发现使用${}获取动态参数会直接替换)
将sql改为使用#{xxx}的方式替换
再次执行测试类看打印结果: (发现使用#{}
获取动态参数是采用占位符也就是预编译的方式替换)
配置xml文件执行结果如下:
原因:
${}
方式是直接替换,此时sql为select * from user where username=username,但是正确的sql应该为select * from user where username=‘username’,程序中执行的sql,admin没有带引号
我们可以手动加引号解决这个问题:(但是会存在SQL注入问题)
模拟登录,因为sql注入一般发生在登录阶段,使用${}获取参数
构建单元测试类,使用正确的用户名和密码:可正确获取用户数据
测试类,将用户密码改为错误的,观察打印结果:获取不到用户数据
测试类,使用错误的用户密码 ' or 1='1
,观察打印结果:
发现尽管输入错误的密码,也是可以获取到用户数据,这就是典型的sql注入
问题
我们再使用#{}方式获取参数,看看是否也可以获取到数据
发现使用错误的密码获取不到数据,也就是使用#{}不存在sql注入
问题
list.stream()
将list
转换为一个流。这个流表示了列表中的元素序列。
forEach(System.out::println)
是一个终止操作,它对流中的每一个元素执行给定的动作。在这个例子中,给定的动作是System.out::println
,这是Java 8中引入的方法引用(Method Reference)语法,它表示System.out.println
方法。也就是说,对流中的每一个元素,都调用System.out.println
方法。//整个表达式的含义是:对列表中的每一个元素,打印其值 list.stream().forEach(System.out::println);
1. 还是因为 #{} 是预编译处理, 所以会给username 加引号, 最后就会变成 '%'username'%', 这样的 SQL 语句在语法上就有错.
2. 而使用 ${} 确实能解决问题, 但是前面讲了它存在 SQL 注入的风险, 不安全,
模糊查询可以使用 MySQL 的内置函数 concat() 来处理, 具体示例如下:
concat('%', #{username}, '%')
这个表达式的意思是将%
字符、username
参数的值、另一个%
字符连接起来,然后在username
字段上执行LIKE查询。因此,如果username
参数的值为zhangsan
,那么这个表达式的结果就会是%zhangsan%
,所以这条SQL语句会查找username
字段中包含zhangsan
的所有记录
定义不同:
#{} 预处理;$ {} 是直接替换。使用不同:
#{}适用于所有类型的参数匹配;但$ {}只适用数值类型。安全性不同:
#{}性能高,并且没有安全问题;但$ {}存在SQL注入
的安全问题。接下来,我们来实现⼀下⽤户的增加、删除和修改的操作,对应使⽤ MyBatis 的标签如下:
此处属于特殊的新增, 所以需要多设置 useGeneratedKeys 参数和 keyProperty 参数
- seGenerateKeys: 设置后会让 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键, 默认值:false.
- keyProperty: 此处设置的就是数据库中的主键在实体类中对应的属性名.
对于查询
绝大多数情况都可以 使用 resultType, 它有很明显的优势, 使用方便, 直接定义到某个实体类即可, 在上面介绍的单表查询, 以及模糊查询都是使用的 resultType, 而 resultMap 使用起来就相对来说要麻烦一些, 但是它可以解决字段名和属性名不相同的情况.
resultMap 主要应用场景
1. 字段名和属性名不同的场景. (针对查询的影响)
2. 一对一, 一对多关系可以使用 resultMap 映射并查询数据.
为什么会有字段名和属性名不同的情况呢?
因为在大公司, Java 程序和数据库是有两个岗位的人去开发的, 不同的岗位在代码上的规范也是不同的, 数据库有数据库的规范, Java 有 Java 的一套规范.所以两拨人去写代码的时候, 难免会出现一些属性名和字段名不相同的情况. (例如当一个字段出现多个单词组合的情况, 数据库可以使用下划线组成, 而 Java 是使用小驼峰. 各有各的规范)
当实体类属性和表中属性不同时:
1. 首先要加上
标签, 它有两个属性, 一个是 id , 一个是 type.
- id 是用来标识当前是属于与哪个 xml 文件的,
它的作用域属于当前 xml 文件, 所以其他 xml 文件下是允许重名的. - type 属性表示要映射的实体类.
2.
标签里面就是用来表示不同字段名和属性名对应的映射关系. column 中写的是字段名, property 中写的是属性名.
标签是用来给主键使用的 标签是给普通字段使用的. 3. 对应的查询标签中的 resultType 改成 resultMap.
结论: resultMap 可以根据实体类来自定义接收属性名,前提是我们在 resultMap 标签中声明了 column(字段名),property(属性名).
解决方案2:除了 resultMap, 其实还可以通过属性重命名的方式来解决解决字段名和属性名不一致的问题
加了@Data注解为什么还要重写toString()?
启动单元测试类,观察结果: (成功获取到我们想要的数据)
一对多的关系,就是对于一个属性,它对映着多个其他的属性。举个例子,一个用户可以写多篇博客,这样就是一对多映射关系的一种体现
MySQL实现多表查询:
代码实现:
启动单元测试类,观察结果:
什么是动态 SQL ? 来看看官方是怎么说的:Mybatis动态sql
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
简言之: 动态 sql 是Mybatis的强大特性之⼀,能够完成不同条件下不同的 sql 拼接.
在注册⽤户的时候,可能会有这样⼀个问题,注册分为两种字段:必填字段和非必填字段,那如果在添加⽤户的时候有不确定的字段传⼊,程序应该如何实现呢❓ 这个时候就需要使⽤动态标签
判断一个参数是否有值的,如果没有值,那么就会隐藏 if 中的SQL
语法如下:
mapper层:
// 添加用户,添加用户时photo是非必传参数
public int addUser2(UserInfo user);
XML层(实现上面的接口):
insert into userinfo(username, password
,photo
)values(#{username}, #{pwd}
,#{photo}
)
测试代码(不传photo值时)
@Test
void addUser2() {
String username = "小诗诗";
String password = "123";
UserEntity user = new UserEntity();
user.setUsername(username);
user.setPwd(password);
int result = userMapper.addUser2(user);
System.out.println("添加: " + result);
}
测试代码(传photo值时):
@Test
void addUser2() {
String username = "小诗诗";
String password = "123";
UserEntity user = new UserEntity();
user.setUsername(username);
user.setPwd(password);
user.setPhoto("cat.png");
int result = userMapper.addUser2(user);
System.out.println("添加: " + result);
}
最主要的作用:去除SQL语句前后某个多余的字符
之前的插入⽤户功能,只是有⼀个 photo字段可能是选填项,如果有多个字段,⼀般考虑使⽤
标签中有如下属性:
trim 标签的四个属性,它们可以达到往 SQL 语句块中拼接或删除字符(串)的作用,从而控制 SQL 的写法。四个属性不是必须的,可以根据语法自行设置。
注意:
标签的
prefix
属性和suffix
属性只在标签包含的内容不为空时生效。也就是说,如果
标签里面没有任何内容(即所有
测试条件都未通过),则
prefix
、prefixOverrides
、suffix
和suffixOverrides
属性都将无效,因为没有内容可以添加前缀或后缀,也没有内容可以被替换或去掉。
语法如下:
...
...
mapper层:
// 添加用户,其中username,password.photo 都是非必传参数,但至少会传递一个参数
public int add3(UserInfo userInfo);
XML层(实现上面的接口):
insert into userinfo
username
password
,photo
values
,#{username}
,#{password}
,#{photo}
测试代码:
@Test
void addUser3() {
String username = "小甜甜";
String password = "123";
UserEntity user = new UserEntity();
user.setUsername(username);
user.setPwd(password);
user.setPhoto("dog.png");
int result = userMapper.addUser2(user);
System.out.println("添加: " + result);
}
结果如下:
听名知意,where想必就是给我们SQL语句中where判读条件使用的。使用where标签时,有以下几个细节:
- 使用where标签时,我们不用再手写sql语句中的where关键字。
- 当where标签中没有任何条件时,where关键字会被省略
- where标签会帮助我们删除SQL中的前缀 ‘and’关键字
- 通trim标签一致,where标签通常也和if标签搭配使用
语法如下:
.....
mapper层:
List getListByIdOrTitle(@Param("id") Integer id,
@Param("title") String title);
测试代码:
WHERE 1=1 的添加是一种常用的 SQL 技巧,用于处理查询中动态添加条件的情况。其主要目的是为了避免编写 SQL 语句时因为多余或缺少 AND 关键字而导致的语法错误。
【小结】
作用:
语法如下:
...
- 此处为什么不包裹后面的 where id=#{id} , 因为你要更新数据, 你
在编写SQL语句的时候,我们可能面临这样的情况:需要删除的ID列表是动态的,也就是说,列表中的元素数量在编写代码的时候是未知的,可能是由用户输入决定的,或者是依赖于其他业务逻辑的结果。例如,用户可能想要删除一个、三个、五个或更多的记录,具体的数量在编写代码的时候是不确定的。
在这种情况下,使用
起码是知道要更新哪一条, 如果没有这条数据, 谈啥更新. 其次就是foreach
标签是非常有用的,因为它可以为你动态地构造出IN语句中的值列表。当你的代码执行到这个SQL语句的时候,MyBatis会看到这个foreach
标签,然后根据你提供的列表中的实际元素数量,动态地生成一个合适的IN语句。这样,你就可以轻松地处理动态数量的删除操作了。标签里面的值一般不会出现全为空的情况, 如果都全为空了, 那就不叫更新了. - 扩展:
标签还可以使用 替换. 但是没必要.
作用: 主要就是对集合进 行循环的
标签有如下属性:
- 在编写SQL语句的时候,我们可能面临这样的情况:需要删除的ID列表是动态的,也就是说,列表中的元素数量在编写代码的时候是未知的,可能是由用户输入决定的,或者是依赖于其他业务逻辑的结果。例如,用户可能想要删除一个、三个、五个或更多的记录,具体的数量在编写代码的时候是不确定的。
- 在这种情况下,使用
foreach
标签是非常有用的,因为它可以为你动态地构造出IN语句中的值列表。当你的代码执行到这个SQL语句的时候,MyBatis会看到这个foreach
标签,然后根据你提供的列表中的实际元素数量,动态地生成一个合适的IN语句。这样,你就可以轻松地处理动态数量的删除操作了。- 注意这个要删除的ID列表是由前端用户选定后传递给后端的。例如,在一个具有批量删除功能的前端页面中,用户可能会选择多个条目,然后点击"删除"按钮,这个时候,前端会把用户选定的所有条目的ID组织成一个列表,然后传递给后端进行处理。后端接收到这个列表后,就会使用这种
foreach
标签的方式,去动态生成一个删除语句,然后执行这个删除语句,从而实现批量删除的功能。
总结动态 SQL