赞
踩
MyBatis 是⼀款优秀的持久层框架,它⽀持⾃定义 SQL、存储过程以及⾼级映射。
MyBatis 去除了⼏乎所有的 JDBC 代码以及设置参数和获取结果集的⼯作。
MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接⼝和 Java POJO(Plain Old Java Objects,普通⽼式 Java 对象)为数据库中的记录。
简单来说 MyBatis 是更简单完成程序和数据库交互的⼯具,也就是更简单的操作和读取数据库⼯具。
针对 MyBatis 是⼀款优秀的持久层框架 进行分析和补充。
MyBatis,也是一个 ORM 框架。
ORM(Object Relational Mapping),即对象关系映射。
在⾯向对象编程语⾔中,将关系型数据库中的数据与对象建⽴起映射关系,进⽽⾃动的完成数据与对象的互相转换:1、 将输⼊数据(即传⼊对象)+SQL 映射成原⽣ SQL
2、将结果集映射为返回对象,即输出对象ORM 把数据库映射为对象:
数据库表(table)>>> 类(class)
记录(record,⾏数据)>>> 对象(object)
字段(field) >>> 对象的属性(attribute)
⼀般的 ORM 框架,会将数据库模型的每张表都映射为⼀个 Java 类。
也就是说使⽤ MyBatis 可以像操作对象⼀样来操作数据库中的表,可以实现对象和数据库表之间的转换。
即:MyBatis 可以称为是一座“桥梁”。
它的作用就是:将 数据库 和 程序,映射起来。
映射起来之后,就可以做一系列的操作了。
比如:
查询,由于我们将 数据库 与 程序映射起来了,直接就把整个数据表映射到一个类里。
查询的结果,就是一个集合,存储的是类的对象(插叙到的一行行记录),那数据也就查询到了。
保存,还是一样的,我们已经将 数据库 与 程序 映射起来了,把表里面的数据,搞到对象里面了。那我们调用对象的某个方法,是不是就可以直接对象的成员直接保存到 MySQL 里面了呢!
MySQL 和 MyBatis,是不一样的。
MySQL,提供了一个 数据存取(数据管理) 的软件。
而 MyBatis 是一个“中间桥梁”,用于连接 程序 和 数据库,建立关系映射的,进行 数据操作 的中间层(持久层)。
总的来说:
MyBatis 的定义:MyBatis 是一个优秀的 ORM(对象关系映射)持久层框架。
其作用:实现 程序 和 数据库 之间的数据交互。
MyBatis ⽀持⾃定义 SQL
自定义SQL,包含的范围就很广了。
数据库中的 “CURD(增删查改)”语句,都是可以实现的。
自定义SQL嘛,我们想写什么SQL,就写什么SQL。
由此,不难看出 MyBatis 一个 最大的特点:灵活!
这也是为什么 MyBatis 在国内能够流行的重要原因!!!
存储过程
我之前将的 SQL,不管是多复杂的SQL,我们都是一行SQL解决。
因为 SQL中,并没有特别复杂的业务,像 for循环,if判断。。。这些都没有。
就是一个 很平滑,单一维度 的 SQL 操作,这是针对于 普通 SQL。
但是!存储过程,是SQL中的方法,它是由一大堆 SQL 的组成的。
这个组成里面,它是有循环的,判断的,分支的,有变量传递的。
这个就叫做存储过程,也就是将 SQL 方法化。
反过来理解:
SQL 方法化 的过程所产生的东西,就叫做存储的过程 。
以后,我们工作了之后,别人给我们一存储过程,里面就是密密麻麻的SQL语句。
还有 if 判断,for循环,设置变量的值…
然后,入参,出参,反正就是很大一串的SQL语句。
但是 总得有人来做吧,这就诞生一个特殊的职业 DBA。
DBA:Database Administrator —— 数据库管理员,又称数据库开发工程师 。
每天都是在捣鼓 SQL 语句的,只玩数据库的,玩数据的。
对于 大部分程序员来说:根本就搞不懂 存储过程 ,这么复杂的东西!
而且,存储过程中有一个特别特别难用的东西!
我们在敲一个很复杂的 java 代码的时候,可以通过 debug 可以一步一步去调试,进入类,方法,属性中,来观察运行时的状态。从而能够帮助我们排查程序中存在的错误。也及时 程序 是能够调试的。
但是 存储过程 是不能调试的。
也就是说:一个存储过程,有这几百行的SQL语句,要我们去写。
而且,出现了错误,只能通过肉眼去排查错误。【不支持调试】
我敢说:百分之90的程序员,都做不到这一点。
这是属于 真正的天才,才能胜任的工作。
高级映射
高级映射,除了可以实现 数据表 和 程序里面的对象 映射之外,还可以实现 一对一的多表映射 和 一对多的映射。
比如: CSDN
每一篇博文,都有它的作者。
博文 与 作者 的关系,就是一对一的关系。
此时,使用 MyBatis 就可以在进行查询的时候,进行 连表查询,并且将 查询到的数据结果,映射到 我的某一个 文章对象里面。
再来,一个作者,他可以发很多篇博文。
站在这个角度,作者 与 他发布的博文,成 一对多的关系。
这种情况,使用 MyBatis 也可以实现这个任务。
MyBatis 可以直接去查询数据库里面的数据,然后,将这些 数据 赋值 给 UserInfo。
这个 UserInfo 里面是有一个属性的(list article - 文章列表)。
就是用来记录这个作者创作的作品。
MyBatis 去除了⼏乎所有的 JDBC 代码以及设置参数和获取结果集的⼯作。
以往,我们在进行 JDBC 编程的时候,需要获取数据源对象(DataSource),然后设置三个属性 URL(数据源路径),用户名(默认是 root),密码(MySQL的登录密码)。
在设置完这三项之后,然后才能与数据连接,进行一系列的操作。
另外在获取 结果集的时候,我们需要使用到一个 类(ResultSet)的对象,来接收数据库返回的结果集,还需要通过 迭代器的方式,才能进行后续的打印。
使用了 MyBatis ,上鞋这些就都不需要了。
它会自动帮我们实现映射,
我们只需要写 SQL 语句(不要写错),写完之后,它会把这个 SQL语句 执行,将 查询到的结果(表数据),直接全部映射到对象里面。
也就是说: JDBC 的前置操作的代码,我们一行都不需要写。
MyBatis 直接全包了,你直接用就行。
MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接⼝和 Java POJO(Plain Old Java Objects,普通⽼式 Java 对象)为数据库中的记录。
MyBatis 在实现的时候,它有两种实现方式:
1、通过 xml 来实现数据的操作,数据操作支持所有的类型。
这是在 MyBatis 3.1 版本之前,主流的一个操作方式。
使用 xml 的形式,来实现 对数据库进行 “CURD” 操作。
2、在3.1 版本之后,MyBatis 有了新的选择。
也就是 第二种方式(注解方式)。
但是目前主流还是使用的 xml 形式。
为什么呢?
MyBatis 使用起来,非常灵活!
灵活是需要付出代价的!
代价,就是我们要去写 SQL语句。
你试想一下:我们在注解中写一个 SQL语句,是不是很别扭?
代码没多少,注解倒是非常长。
所以,MyBatis 在 3.1版本之后, 提供了 注解 这个新的操作数据方式。
它也没有用,因为不好用。
注解的方式,用来实现简单 SQL 操作,还是可以的。
但是!一旦SQL语句复杂了一点,你在使用的时候,就会特别别扭。
注解 比 代码,还有长。。。
而 xml 写SQL语句比较灵活,也不会感觉到别扭,好用!
所以,本文也是 基于 xml 来实现 对数据库 进行操作的。
对于后端开发来说,程序是由以下两个重要的部分组成的
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、 释放资源
对于 JDBC 来说,整个操作⾮常的繁琐,我们不但要拼接每⼀个参数,⽽且还要按照模板代码的⽅式,⼀步步的操作数据库,并且在每次操作完,还要⼿动关闭连接等,⽽所有的这些操作步骤都需要在每个⽅法中重复书写。于是我们就想,那有没有⼀种⽅法,可以更简单、更⽅便的操作数据库呢?
答案是肯定的,这就是我们要学习 MyBatis 的真正原因,它可以帮助我们更⽅便、更快速的操作数据 库。
之前使用 jdbc 需要进行九步操作,现在使用了 MyBatis,就写一个方法,一个 SQL就搞定了。 像什么手动释放资源,处理结果集,都不用我们去做了。MyBatis “保姆级” 助手。
本文重点分为两个部分:
1、配置 MyBatis 开发环境(创建一个 SSM 项目)
2、使用 MyBatis 模式和语法 操作数据库
下面我们会创建一个关于 博客 的 数据库 mycnblog
并且在 mycnblog 数据库中创建三张表。
- -- 创建数据库
- 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);
打开你们本地的MySQL,将上述SQL,复制粘贴到 MySQL中。
PS: 建议先拷贝到 一个文本文件中,然后再拷贝到 MySQL中。
因为你如果直接拷贝上面的内容,SQL就会挤成一坨。
也就是说不带有格式的。
你把上面代码先放在笔记本中,再粘贴到MySQL中。
不但没有挤在一起,而且SQL全部执行了,只有最后一句SQL没有执行。
直接回车,就全部OK了。
我们下面来看看数据,有没有插入成功
我们要想使用 MyBatis,光引入 MyBatis 依赖,是不够的。
我们需要引入一个依赖(关于数据库的)。
这里就会涉及到两个场景:
1、项目创建的时候,引入 MyBatis 相关依赖
2、老项目,添加 MyBatis
使用 Edit Starter 插件 添加
不要立即启动项目!!!!
如果立即启动项目,就会报错。
社区版:
这是因为 没有配置 关于 数据库的数据源的 URL
我们需要配置
此时,我们再启动项目,就不会报错了。
xml 是一个 资源文件,对不对?
那么,xml 文件,还是放在 resource 目录下。
还有一个问题:
放在 resource 哪个 目录底下。
需要注意的是:一般是不会将其放在 resource 根目录底下,与配置文件处于同一级目录。
因为到时候,配置文件,非常多!
此时,你放进去,不就是添乱嘛!!!
通常我们都是在resource 目录下,创建一个子目录,用来存放的。
MyBatis 的 操作模式,包含两个部分:
1、Interface(方法定义)
2、xxx.xml(方法实现)
注意!这里是接口,不是类。
这个interface 中 会加一个注解@Mapper,它是来自于mybatis里面的注解 Mapper。
加了这个注解之后,表示我们当前的这个 mybatis 类 所的事情,就是 实现 对象的映射。
这个时候,帮我们的方法名字 定义到 interface 里面。
我们 interface 里面的方法,是不能有实现的,是一个抽象方法。
思考一下:
假设,我们要根据用户ID 去查询一个用户信息。
那我们定义一个方法名有什么用?又没有具体实现,怎么可能会实现查询功能??
答案显而易见是不行的!!!
'这个时候,我们就需要另一个东西:xml 文件
mybatis 想操作数据库,必须得有两个文件。
第一个,它是Java中的 一个普通接口,在接口中,定义方法名称。
定义方法名称之后,就该实现方法了。
但是接口中方法不能有实体,需要通过一个注解来进行映射方法,映射一个 xml 文件中。
当然,在xml文件中,也需要表明它是那个方法的映射。
这就是第二个文件 xml。
根据我们前面讲的映射:就是 把业务代码 转换成 SQL 语句。
也就是说我们 定义在接口中的方法,是为了声明该方法 对数据 进行 何种操作。
具体的实现 已经通过某种映射关系,映射到 xml 文件中,
我们需要在 xml 文件中,编写 对应功能的 SQL 语句,就可以了。
在xml里面,只需要配置 我们要是实现的 interface 是谁,我要实现的 SQL 是谁。
OK ,这样一配置就完了。
这两个合在一起,最终生成 mybatis 中能执行的SQL。
通过这个SQL语句,去操作数据库。
将 操作数据库得到的结果,返回给 服务层。
服务层,再返回给 controller(控制层)。
控制层,再把结果交给用户。
这样 就完成了 一次交互。
所以,我们讲 mybatis 的 操作模式,就是在讲下图中的两个文件。
这两个配合起来,就能生成 数据库可以执行的SQL语句,并且执行 SQL,操作数据库。
并将 结果 映射到程序的对象中。
我们要实现一个 根据 用户id 来查询用户信息的操作。
前面我们在准备工作中,就是已经给 userinfo 表中插入了一个数据。
xml 文件,不能随便创建。
我们在配置文件中,已经指定了 xml 文件存储路径。
并且,命名规则也指定了,我们必须按照规则来。
至于 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="">
- </mapper>
相信很多朋友,都会发现 mapper标签中的 namespace 属性,缺少value值。
namespace 的值,是需要我们手动去填写的。
填写的内容:是需要时实现的接口位置 >> 包名 + 接口名称。
接下来,就是在 xml 文件中,实现 UserInfoMapper 接口 中的 方法。
我们在 service 包底下创建一个 UserInfoService 类。
在里面 写一个方法, 去 调用 UserInfoMapper 中的方法。
然后,在 controller 包下,也去创建这样的一个类,去调用 UserInfoService 中的方法。
此时,我们将项目启动起来。
我们在写 mybatis 代码的时候,无非就是两个操作:1、创建接口,定义方法;2、在写满了文件中编写SQL。
整个 mybatis 操作 完成了。
这里有一个比较麻烦的地方:
就是在我们写完功能之后,我们要进行测试的话,成本很高!mapper的内容写完,我们还需要写 service,写 controller,还没完!
我们还需要打开 浏览器方法(或者利用postman来模拟访问)。
虽然整个流程,非常有序,一环接一环。
但是!这会让人感觉很拖!
给人的感觉就是:代码被强行 “ 拉长 ” 了。
那有没有什么方法,能让我们写 mapper 内容之后,直接可以进行测试呢?
这就是下面,我要讲的东西:Spring Boot的单元测试
单元测试(unit testing),是指对软件(项目)中的最⼩可测试单元进⾏检查和验证的过程就叫单元测试。
单元测试是开发者编写的⼀⼩段代码,⽤于检验被测代码的⼀个很⼩的、很明确的(代码)功能是否正确。
执⾏单元测试就是为了证明某段代码的执⾏结果是否符合我们的预期。如果测试结果符合我们的预期,称之为测试通过,否则就是测试未通过(或者叫测试失败)。
最小可测试单元:方法
每一个方法都代表一个相应的功能。
那我们测试的最小单元,对于 Spring Boot 来说,就是一个方法。
OK,你写一个方法,我测一个方法。
测试方法,就是测试一个单元。
方法,是不可以再被分割的!
你不可能说: 我们去测试 方法中的某一段程序,或者说测试某一个属性。
因为,这叫做 调试。
调试 和 测试单元,是两码事!!!!
但是!有的朋友可能会说:
方法中,经常会调用另一个方法,来“辅助”自身的运行。
那这种怎么说?
如果方法中还有方法,这个时候,单元测试就需要分为多个了。
首先是这个方法中所有依赖的方法,我们可以对这里面所有的方法,做一个单元测试。
最后,再回到本身,对本身的方法进行单元测试。
1、单元测试不用启动 Tomcat
对于这一点,不好说。这个说法,要分角度来看。
从自身的角度来看:我们确实没有启动 Tomcat,但是 Spring Boot 自动启动了 Tomcat。
因为 Spring Boot 内置了 Tomcat。
其实 Spring Boot 除了支持 Tomcat,还支持其它的Web容器。
2、如果中途修改了代码,在项目打包的时候会发现错误,因为打包的时候会自动执行单元测试,单元测试错误就会发现。
这一点最重要!
3、单元测试,最大的一个好处 :
让我们非常方便,非常直观,也非常直接的,在写完一个功能之后,立马就能知道这个功能是否是正确的!
这也是我们学习单元测试的初衷,就是为了降低 测试 mybatis 功能的成本。
4、不使用单元测试的时候,它是会 “ 污染 ” 本地的数据库的!
就是比如说:
我们实现了一个 添加 功能,我们要去测这个添加功能。
这就需要正儿八经的去进行添加操作的。
这就会对本地数据库中的数据,发送变动。
这就是 “污染”!
然而,如果使用了 单元测试 来测试功能,.
测试单元,就能保证在 测试完功能之后,对于数据库中的数据没有任何影响!
这个牵扯到了 事务的回滚机制。
单元测试,也是通过利用 数据库的回滚机制 来 避免对 数据产生 “ 污染 ” 。
也就是说:我们在执行单元测试之前,MySql 为其创建了一个事务。
【MySQL 默认情况下,是处于开启事务的状态,也就说开启了事务的自动提交】
然后,开始执行单元测试,对数据库中的数据进行操作。
在验证完 单元的功能没有问题之后,就会进行事务的回滚操作。
还原到 还没有测试的情况。【将数据还原到初始状态】
Spring Boot 项⽬创建时会默认单元测试框架 spring-boot-starter-test,⽽这个单元测试框架主要是依靠另⼀个著名的测试框架 JUnit 实现的,打开 pom.xml 就可以看到,以下信息是 Spring Boot 项⽬创建是⾃动添加的:
这里有一个细节:
这个时候,此⽅法是不能调⽤到任何单元测试的⽅法的.
此类只⽣成了单元测试的框架类,具体的业务代码要⾃⼰填充。
至于为什么还需要加一个 测试注解,是因为 我们生成测试类,是一个普通的class类。
我们需要在测试类上 声明它要测试的方法,是运行在 Spring Boot 容器当中的。
所以,@SpringBootTest 注解,就是起着这样的一个声明作用。
表示当前这个类中 测试方法(测试单元) 是运行在 Spring Boot 中的。
虽然,从打印的结果来看,功能是没有的。
而且,左边那几个 绿色勾勾,代表测试通过。
但是! 我们是使用 的 sout 语句,来对查询结果进行输出。
这并不是 我们 单元测试的效果!!!!
单元测试的效果应该是 单元测试中的断言!!!
断言:就是以什么为标准,
通过这个标准来衡量 测试是否通过。
比如,如果 断言的结果为 true,就表示 单元测试 通过了。
反之,断言的结果为 false,就表示 单元测试 不通过。
并且!断言之后的代码,是不再执行的!!!
由 Assertions 类提供的,注意!这个类是 JUnit 提供的
下面,来跟着我看一下,如何使用断言,来判断单元测试是否通过!
这就是断言的魅力,一旦出错。
如果测试失败,它的报错信息会非常非常的详细!
后面,我们实现 增,删,该2的时候,就不需要 浏览器 和 postman了。
直接使用断言,来测试即可。
能够帮助我们快速切换 接口 和 xml。
让我们操作 MyBatus 更舒服。
而且,还能帮我们自动去生成代码。
接下来,我们来实现⼀下⽤户的增加、删除和修改的操作,对应使⽤ MyBatis 的标签如下:
< insert >标签:插⼊语句
< update >标签:修改语句
< delete >标签:删除语句
接口的定义,还是一样的写法。
写法的差异,主要就体现在 xml 中 的 自定义 SQL 语句 上。
我们先给 userinfo表插入一条记录。
下面,实现 改操作。
下面我们来拓展几个细节:
1、方法参数 使用 @Param 注解之后,原先参数的名称不能再使用。
得出结论:
@Param 注解 和 @RequestParam 注解,起着相同的作用。
方法 参数重命名。一旦重命名之后,原来的名称是不能使用的。
只不过 @Param 注解中的参数是 重命名后的结果。
而 @RequestParam 注解中的参数 是参数原名,方法参数是 重命名的结果。
于此同时,我们又发现了一个问题。
当我们测试完之后,我发现此时数据中的数据被 “污染”了!
前面不是说:单元测试不会污染 数据库中的数据嘛?
下面,我们就来实现这个功能。
方法很贱简单,在测试的单元上,加上一个注解@Transactional,就行了。
@Transactional:就是事务的意思。
其作用就是在开始执行测试之前,开启一个事务。
这个事务完成的任务,就是我们定义的是SQL操作。
在 操作完成之后,也就是测试完成之后,该事务会进行一个 “ 回滚 ” 操作。
原来的数据是什么样子的,现在还是什么样子的。
就是说:事务执行的期间确实 “ 污染 ” 了 数据库中的数据,
但是!在事务执行完成之后,事务会通过 “ 回滚 ” 操作,将被 操作的数据还原到未修改的状态。
这里需要注意一点:
@Transactional 注解,原本只是提供一个 自动提交事务的功能。
并不会提供 自动 “ 回滚 ” 功能。
只是说 该 单元测试 所处于的 测试类,是被 @SpringBootTest 注解 修饰的。
等于就是说:“强行” 给 @Transactional “ 加班 ”!!!
无论 单元测试是否成功,都会进行一个 事务的 “ 回滚 ”,将数据库的数据还原!只需要在 测试的单元上,再加上一个 @Transactional 注解,就可以在 “ 不污染 ” 数据库中数据的前提下,完成单元测试。
当然,这个操作的大前提:
是在 测试的单元所在的测试类,是被 @SpringBootTest 注解所修饰的。
注意!只有 select 标签,特殊一点(需要指定返回类型)。
delete 标签 和 update 标签都是一样的。
这个插入操作,相比前面 “ 删查改 ” 操作,要复杂一些。
所以,把它放在最后来讲。
操作还是一样的,只是插入的值,是一个对象,在 编写 SQL的时候,如何获取对象的属性,是一个问题。
还有返回值的处理。
返回值,返回一个受影响的行数
现在我们不想 将 影响的行数,作为返回值了。
我要 插入成功的用户信息的ID!
虽然实现的步骤没有变化,但是想要达到预期的效果,还需要做点事。先来看第二步:在xml文件中 编写 SQL语句 的相关代码。
在上述 insert 语句中,加属性:
useGeneratedKeys:
这会令 MyBatis 使⽤ JDBC 的 getGeneratedKeys ⽅法来取出由数据库内部⽣成的主键
(⽐如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的⾃动递增字段),默认值:false。
keyColumn:
设置⽣成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第⼀列的时候,是必须设置的。如果⽣成列不⽌⼀个,可以⽤逗号分隔多个属性名称。简单来说:我们加上 keyColumn 的原因,就是为了确保获取 对应字段。
keyProperty:
指定能够唯⼀识别对象的属性,MyBatis 会使⽤ getGeneratedKeys 的返回值 或 insert 语句的 selectKey ⼦元素设置它的值,默认值:未设置(unset)。如果⽣成列不⽌⼀个,可以⽤逗号分隔多个属性名称。
再来看一步:在 mapper 中的 UserInfoMapper 接口中定义 方法。
返回受影响的行数 和 自增 id
测试一下
有的人可能会有疑问:自增ID,是有什么问题吗?
其实没问题!
这是因为 MySQL 中的 搜索引擎 InnoDB 的机制,就是这样设计的。
学到这里,就可以将一些 简单的servlet 项目,改成 SSM 项目。
预编译处理是指:MyBatis 在处理#{}时,会将 SQL 中的 #{} 替换为?号,使⽤ PreparedStatement 的 set ⽅法来赋值。直接替换:
而MyBatis 在处理 ${} 时,就是把 ${} 直接替换成变量的值。
这里需要普及的一点:
MySQL中有两种查询:1、叫做及时查询(字符直接替换);2、预查询(预处理)
及时查询:当了需要执行SQL的时候,才会去执行SQL,
预查询:相对于 及时查询,预查询要多出两步:既然查询的参数会变,那先用一个 ?号占位符 顶替参数,先构造好一个SQL,进行预查询,看看SQL的格式有没有错误。
随后,通过 jdbc 的方式来替换 占位符。这样就能保证SQL的正确性。
你可能觉得这麻烦,但是其实!预查询是为了提高查询效率的。
举个例子:
通过上述的代码,只能证明 #{} 和 ${} 对于 查询条件为整形的查询效果是一样,至于效率,只能意会,毕竟,我们感知不到。
但是!安全性,是可以体现的!下面我们就来演示一下。
由此,不难得出结论:#{}支持所有的数据类型,${} 只支持 数值类型
再举个例子,来加深对 预编译【 #{} 】和字符串替换【 ${} 】之间的区别 的理解
头等舱和经济舱乘机分离的故事:
⼀般航空公司的客运机都是头等舱和经济舱分离的,头等舱的⼈先登机,登机完之后,封闭头等舱,然后再让经济舱的乘客登机,这样的好处是可以避免有人浑⽔摸⻥,经济舱的⼈混到头等舱的情况,这就相当于预处理,可以解决程序中不安全(越权处理)的问题。
⽽直接替换的情况相当于,头等舱和经济舱不分离的情况,这样经济舱的乘客在通过安检之后可能越权摸到头等舱,如下图所示:
这就是 直接替换【 ${} 】 所带来的缺陷。
就是说: SQL 没有正确访问它应该访问的地方。
这就是 SQL注入的安全问题。
而且,由于头等舱的票很贵,买的很少。
通常是坐不满的,因此,上述这种 经济舱的人 做到 头等舱也是常有的 。
最好的方法,就是前面说的一下,先让头等舱上机,上完之后。
关闭头等舱的入口,然后,经济舱的人开始上机。
这样就不会存在有 “ 误入 ” 的情况了。
所以,出现这种问题的根本原因:就是等级没有分明。
头等舱的门,就相当于是一个界限。
将 头等舱 与 经济舱 分离了。
这样就不用做第二次校验,也就不会产生纠纷问题。
即使 头等舱的用户来迟了,叫一个空乘服务人员,专门领他过去,就行了。
领进去,再把门一关。
经济舱的人还是混不进入的。
也就是说:每个人都只经历了一次检验机票,就完成了登机。
那么,问题来了。
既然 #{} 这么无敌,那么,${} 是不是就可以滚蛋了?
NO!
其实,与其说是 ${} 的优点,不如说是它的专长!
毕竟 这任务,#{} 做不了,只有它能做!
前面不是说:
使用 ${} 存在 SQL 注入的问题嘛?
现在,哪怕是处于 ${} 的主场,依然也存在着这个问题!
那如何处理呢?
注意事项:
当不得不使用 ${} 时,那么一定要在业务代码中,对传递的值,进行安全校验!
就是说:当代码执行到 mapper 这一层,就没救了!
只能 “ 上战场 ”了。。背水一战。
所以,一定要在业务代码中,对数据校验。
Controller 的作用就体现出来了!
因为 它就是负责对数据的校验。
结合现在的情况:就是对 传递过来的 order 进行 判断非空,长度不能为0,甚至对内容进行校验,是否是 desc 和 asc,其中的一个。
结合生活案例:
我们在上地铁,火车,飞机等公共交通工具的时候,为什么在我们乘坐之前,就开始检查我们的包裹,而不是 上车的时候,检查包裹。
这我们的 ${} 的校验数据,是同一个道理。
为了保证传递的数据的安全性以及正确性。【确认没有违规物品,保证乘客的声明安全】
由上面的例子,相信大家对 SQL注入问题,有了一定了解。
SQL注入问题,就是在我们没有对 传递数据进行校验的前提下。
当我们处于一些特殊场景的时候,可以在不满足获取条件的情况下,获取私密信息。
这对用户信息和 财产的安全隐患是非常巨大的!!!
这就是安全漏洞啊!!!
所以,请一定一定一定切记!在处理一些重要信息的时候,如果使用了 ${},请在 Controller 层,对数据进行严格校验!
我们再来试一下:把 $ 改成 # ,效果会如何?
由此,不难得出结论:
使用 #{} 是不存在 安全漏洞滴!!!
是非常nice的。
前面的问题,由于关键字,就那么几个。
直接穷举,很容易在 Controller 层里面 判断数据的正确性。
但是!模糊匹配能穷举吗?
很显然是不能的!
如果数据有个几百万,那我们不得嗝屁!
下面,演示一下。
此时,问题也就随之而来了!
在进行模糊匹配查询的时候,不能使用 #{} ,否则会报错!
而改用 ${} ,有穷举不了所有的情况。
无法做到完美验证数据的正确性和安全性。
那么,怎么解决这一个问题呢?
使用concat拼接方法,就可以解决问题了。
#{} 和 ${} 的区别总结
1、定义不同:#{} 预处理:而 ${} 是直接替换
2、使用不同:#{} 适用于所有类型的参数匹配;但 ${} 只适用于数值类型。
3、安全性不同:#{} 性能高,并且没有安全问题;但 $ {} 存在 SQL 注入的安全问题。
4、使用场景不同:
当传递的是一个 SQL 关键字 的时候,只能 使用 ${} 。
当传递的是一个字段,总之,就是需要获取到参数类型 与 内容,只能使用 #{}。
(PS:数字类型,#{} 和 ${} 都是可以使用的!)
5、 ${} 不能用于 模糊匹配查询;而 #{} 需要搭配 concat 才能在模糊匹配中使用。
除了修改 实体类属性名称,还有一个方法:使用 下面即将介绍 resultMap。
返回字典映射: resultMap
resultMap 使用场景:
1、字段名称和程序中的属性名不同的情况,可以使用 resultMap 配置映射、
这个上面演示过了,就不再演示了。
2、一对一和多对多关系可以使用 resultMap 映射并查询数据。resultType && resultMap 的区别
1、在对象属性名称 与 数据表字段名称,相同的情况下:
使用 resultType 比 resultMap 更爽!
2、在对象属性名称 与 数据表字段名称,不同的情况下:
使用 resultMap 可以指定 不同名称的 字段与属性 的映射关系。
在 MyBatis 中,支持的多表查询有两种:
1、 一对一关系:以博客来说,一篇博文只能有一个作者,文章和作者的关系就是一对一。
2、一对多关系:一个作者可以是 多篇博文的作者。都是他写的嘛!作者 和 文章是 一对多的关系。
下面,我就来模拟实现一下 一对一的关系。
but!在此之前,我们需要创建关于文章表的实体类。
我们前面只是创建一个 用户表的实体类。
在进行查询之前,我们先来看一下 articleInfo 表中,有几条信息,信息的内容是什么,这样方便我们后面写代码。下面,我们去mapper 包中,在创建一个 MyBatis 的接口。
实现根据文章的 id 查询到文章的详情信息。
咋一看,好像没有问题。但是确实有问题!
这就很好奇了,文章的 uid 是 1,对应着 用户表中 admin 用户。
就是说:文章的作者,确实是存在的。
那为什么 userinfo是 null 呢?
原因很贱,我们的实体类有这个属性,但是文章表里没有这个字段啊!
这个时间,resultMap 就上场了。因为 resultType 它不行!
此时,我们就实现了一对一查询。(存在问题)
【association 标签,就是用来实现一对一情况的多表查询】
虽然存在一些问题:
UserInfoMapper 中 resultMap 配置的字段信息不完整。
导致:虽然查询结果的信息是完整,但是没有完整映射到 ArticleInfoMapper 的 userinfo 属性中。
这里可以体现出一个结论:
在 本身 xml 文件,是可以不用映射所有属性的信息。
因为 它是自己调用自己,所以,不需要 resultMap 将属性全部映射,都能自动完成所有属性的映射。
而 想要在一个 resultMap 中 调用 另一个 resultMap 中信息,只能是它映射了的信息。
否则无法获取。
所以,我们来讲 UserInfoMapper 中 resultMap 对于属性的映射补全,再来看个效果。
这里还存在着问题:
到头来,结果发现:还是存在问题的。
当一个属性,在两个数据表中都存在时,默认读取的是 本身的字段值。
也就是说:不光是上面的两个,我们演示的那个情况,出现的那四个属性都是这个情况。
下面,我们来分析解决这个问题。
同时加深对这个问题的印象。
这个时候,我们才真正完成了 一对一关系的多表查询!!!!!
一对多关系:⼀个⽤户可以是多篇⽂章的作者。
⼀对多需要使⽤ < collection > 标签,⽤法和 < association > 是一样的 .
为什么使用 collection标签,其实很理解。
看中文意思就明白了!
association 的中文就是关联,放到使用场景中,就是将 2个表联系起来。
collection 的中文意思就是 收集(把它理解为集合),将多个表收集起来,整合。
当然,这些表,肯定都与一张表有关系,不管怎么叫做 一对多?
放在实例中,多个表对应着多篇文章,
这多个表中有一个相同字段,且信息是一样的。【作者id是一样,即是同一个人写的】
由这个作者id,就可以把所有文章(是他写的文章)给映射起来,建立关系。
这就是 一对多 关系。
在实现一对多关系查询之前,我们先来做一些准备工作。
首先我们来用户表的实体类进行处理。
下面我就来实现这个一对多的关系查询。。
实现一个 根据用户id,获取用户和他缩写的文章信息。
换句来说:
一对多关系的查询,相比于 一对一关系的查询,就是把 resultMap 中的 association 标签,换成了 collection 标签。
里面的内容一点都没改。
动态 sql 是MyBatis的强⼤特性之⼀,能够完成不同条件下不同的 sql 拼接。
在注册⽤户的时候,可能会有这样⼀个问题,如下图所示:
回顾我们之前实现的功能查询,你会发现参数全部都是确定的,就是写死了的。
都是抱着那种前端一定会传给我数据的 “ 态度 ”,也就是认为一定会拿到必要参数!
但是!在实际业务场景中,会存在一些非必传的参数。
就是说:必要参数,有时候是不会传输的,很有可能传过来的参数,是没有的。
那么,面对这种情况,我们在程序中该如何处理?
下面,我们就来具体实现这个业务。
但是存在着一个问题:如果参数很多,而且!每一个参数都是非必传参数。
那么,我们需要对每一个参数进 if 标签来判断。
这就非常的麻烦!可能会多敲字符,导致SQL出现问题。
这个时候,就需要借助 trim 标签了。
具体怎么使用,看下面对 trim 标签的介绍和使用。
之前的插⼊⽤户功能,只是有⼀个字段可能是非必传,如果有多个字段,⼀般考虑使⽤ < trim >标签 结合 < if >标签,对多个字段都采取动态⽣成的⽅式。
< trim >标签中有如下属性:
prefix:表示整个语句块,以prefix的值作为前缀
suffix:表示整个语句块,以suffix的值作为后缀
prefixOverrides:表示整个语句块要去除掉的前缀
suffixOverrides:表示整个语句块要去除掉的后缀
说白了,trim 就是为了方便程序员 去除 开头 /末尾 的 某个符号。
也就是说:在 开头/结尾 ,多写了这个符号,是没有问题的。
因为 trim 会将它删除掉。
如果没多写,也不会影响 程序的运行。
下面我们来实战演练一波。
传⼊的⽤户对象,根据属性做where条件查询,⽤户对象中属性不为 null 的,都为查询条件。
如user.username 为 “a”,则查询条件为 where username=“a”:
where标签,主要作用:实现查询中的 where 替换的。
什么意思呢?
原先我们写的 where 是 SQL 的一部分,是SQL的关键字。
现在我们使用 MyBaits 提供是 where 标签来代替 SQL 中 where 关键字 。
那么,有的人可能会有疑问; 这替换跟没替换一样,还多敲 几个箭头。
因此,会产生一个问题: 使用 where 标签 的好处都有哪些?
不然不会创造出一个 where标签出来。
如果没有任何查询条件的情况下,where 标签 可以实现 隐藏 SQL 的 where 的SQL部分;但如果存在查询条件,那么会生成 where的 SQL,并且 where 标签可以自动的去除最后一个 判断条件的 and 字符。
也就是说:我们在拼接多个条件的时候,有几个条件涉及到非必传参数。
会有一个什么情况呢?
通常 我们拼接多个条件的时候,要么使用 or(或逻辑),要么使用 and(与逻辑)。
A and B;
当 A 条件,缺少判断的参数的时候,就会将 A 和 后面 的 and 去除掉。
这个and也就是最前面一个 and 嘛。
就变成一格判断条件的 SQL了。
下面,我们来实战一下。
虽然比直接敲SQL,要复杂一点点。
但是,构造 SQL的灵活性,大大提高!
还有一个去除 and 的功能,对吧?
我们再来搞一下。
如果 and 放在后面,就是说 and 后面没有判断条件,或者说 缺少 参数,无法构造 where SQL。会出现什么状况?
其实这个问题也很好解决。直接给你们演示个大概。
以上标签也可以使⽤ < trim prefix=“where” prefixOverrides=“and” > 替换。
无非就是 少用几个属性。
根据传⼊的⽤户对象属性来更新⽤户数据,可以使⽤标签来指定动态内容。
UserMapper 接⼝中修改⽤户⽅法:根据传⼊的⽤户 id 属性,修改其他不为 null 的属性:
这么说吧。set 标签,和 where 是一样。
都是替代 SQL 中的关键字。
set 就是 update 操作所需要使用的关键字。
而且,也是和 if 标签 配合使用的。
而且和 trim 标签一样。可以去掉最后一个符号(一般都是逗号)。
实战演示一波。
那如果没有 逗号呢?会是什么效果?
下面我们把三个非必传参数都传递。
以上标签也可以使⽤ < trim prefix=“set” suffixOverrides=“,” > 替换。
对集合进⾏遍历时可以使⽤该标签。< foreach >标签有如下属性:
collection:绑定⽅法参数中的集合,如 List,Set,Map或数组对象
item:遍历时的每⼀个对象(集合中的元素)
open:语句块开头的字符串(类似 trim的 prefix)
close:语句块结束的字符串(类似 trim的 close)
separator:每次遍历之间间隔的字符串(间隔符)
【PS:foreach 标签,是 trim 所不能替代的!】
应用场景:
通常我们要删除数据库中的信息,不是一条一套的删。
而是一删一大把。
比如:
我们要根据id来删除 记录,我们就把这些id 弄成一个结合。此时就可以通过 foreach 标签,进行遍历传参,删除对应的信息。
这样做的效率就很高,一次调用,就可以将需要删除的数据全部删除。
实战演示一波。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。