1. XML 配置文件
使用 MyBatis 操作数据库的方式有两种:
- 注解 (在注解中定义 SQL 语句)
- XML 配置文件 (在 XML 文件中定义 SQL 语句)
在上一篇博客中, 已经讲解了如何使用注解操作数据库, 本篇文章来讲解如何使用 XML 进行 MyBatis 开发.
使用 XML 的步骤, 和使用注解的步骤是一致的:
- 导入 MyBatis 依赖和 MySQL 驱动的依赖
- 配置数据库信息(连接数据库)
- 定义接口
- 使用 XML 定义 SQL, 操作数据库
1.1 如何配置 XML 文件
1.1.1 步骤一: 导入相关依赖
首先, 需要导入 MyBatis 依赖和 MySQL 驱动依赖, 上篇文章已经讲过, 这里就不再赘述:
1.1.2 步骤二: 配置数据库信息
然后, 需要配置数据库信息, 进行数据库连接, 上篇博客中也讲了, 也就不再赘述.
# 数据库配置
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
1.1.3 步骤三: 定义 Mapper 接口
定义 Mapper 接口, 接口中包含操作数据库的方法:
1.1.4 步骤四: 配置 XML 文件
既然使用 XML 操作数据库, 那么首先需要新建一个 XML 文件:
(注意: XML 文件需要建于 resources 包下!!)
为了提高代码可读性, 可维护性, 通常一个 XML 文件, 对应一个 MyBatis 的 Mapper 接口, 并且建议 XML 文件的命名与 Mapper 接口的命名保持一致:
在 XML 文件中, 需要配置对应 Mapper 接口的路径(接口的完全限定名).
通过 namespace 属性来关联 XML 文件和对应的 Mapper 接口(告诉 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 接口的路径-->
<mapper namespace="com.study.mybatis.Mapper.UserInfoMapperXML">
</mapper>
此外, 还需要在配置文件中, 配置 XML 文件的路径信息, 以便在项目启动时, 能够加载该 XML 文件:
mybatis:
# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
mapper-locations: classpath:mapper/*Mapper.xml
完成以上配置后, 我们就可以通过 MyBatis 以 XML 的形式来操作数据库了.
2. MyBatisX 插件
使用 XML 操作数据库前, 我们需要下载 MyBatisX 插件, 可以帮助我们简化相关操作:
3. 使用 XML 进行 CRUD
3.1 查询数据(<select>)
使用 xml 操作数据库, 不再使用注解定义 SQL, 而是在标签中定义 SQL.
在接口中声明方法, alt + 回车, MyBatisX(插件) 会根据方法的名称, 自动创建相应的标签(由于我的方法名为 selectAll, 所以生成的就是 <select> 标签), 接下来就可以在标签中定义查询相关的 SQL 语句:
其中, 标签中 id 属性的值, 就是对应的 Mapper 接口中方法的名称, 表示在调用该方法时, 执行标签中的 SQL 语句(将 SQL 和方法映射起来).
此外, select 标签中, 还有一个 resultType 属性, 表示查询结果集中, 每条记录的类型:
(只有 select 标签有这个属性, 因为只有查询操作才会返回结果集)
创建测试方法, 执行观察结果:
3.1.1 <resultMap> 标签
在上篇博客中讲到, 对于注解, 如果要实现 Java 属性和数据库字段的映射, 有三种方式:
- 数据库字段起别名
- @Results 注解
- 配置驼峰转换
对于 XML 来讲, 第一种和第三种同样适用. 由于我在上篇博客中已经进行了 驼峰转换 的配置, 因此在上文的 select 查询中, 数据库字段和 Java 属性能够映射成功.
但是, @Results 对于 XML 就不适用了, 在 XML 中, 需要使用 <resultMap> 标签进行映射的绑定:
- 设置 type 属性, 表示和哪个 Java 类型中的属性进行映射关系的绑定
- <id> 子标签, 配置主键的映射关系
- <result> 子标签, 配置其他非主键字段的映射关系
子标签中的映射方式, 和 @Results 一模一样: property 表示 Java 属性名, column 表示数据库字段名.
并且, 对 <resultMap> 设置 id 属性, 将其他 select 标签的 resultMap 属性设置为该 id 值, 就可直接使用对应的 <resultMap> 中的映射关系:
这样配置后, 便将数据库字段和 Java 属性成功进行映射了:
注意:
- 如果使用 <resultMap> 的方式进行映射绑定, 即使数据库字段和 Java 属性本来就能成功映射, 但是也建议在 <resultMap> 中将所有 Java 属性和数据库字段全部手动进行映射绑定一遍!!
- 使用 XML 传递参数的方式和使用注解传递参数的方式都是一样的, 在 SQL 中都是使用 #{} 接收.
3.2 插入数据(<insert>)
给方法传入一个对象, 将对象中的一些属性值作为插入值, 插入表中:
同样, 若对象使用 @Param 进行重命名, SQL 中的 #{} 依旧需要使用 对象名.属性 的形式来声明参数:
此外, insert 标签中, 同样也是设置 useGenerateKey = true 和 keyProperty = "Java 属性" 的方式来获取新增记录的主键:
3.3 更新数据(<update>)
通过 XML 更新数据, 使用 <update> 标签.
3.4 删除数据(<delete>)
通过 MyBatis 使用 XML 删除数据库表中的记录, 使用 <delete> 标签.
4. 其它查询
4.1 联合查询
4.1.1 准备工作
联合查询需要用到多个表, 我们再创建一个 article_info 表, 并插入数据:
-- 创建文章表
DROP TABLE IF EXISTS article_info;
CREATE TABLE article_info (
id INT PRIMARY KEY auto_increment,
title VARCHAR ( 100 ) NOT NULL,
content TEXT NOT NULL,
uid INT NOT NULL,
delete_flag TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
create_time DATETIME DEFAULT now(),
update_time DATETIME DEFAULT now()
) DEFAULT charset 'utf8mb4';
-- 插入测试数据
INSERT INTO article_info ( title, content, uid ) VALUES ( 'Java', 'Java正文', 1 );
并且, 创建一个 Java 类和表中字段相映射:
4.1.2 执行查询
联合查询虽然使用了多个表, 但其本质仍然是 select 语句, 我们直接在 <select> 标签中定义 SQL 即可.
将 article_info 和 user_info 两表联合查询, 以 article_info 为基准(左外连接), 联合查询 article_info 中 id = 1, 并且 article_info.uid = user_info.id 的记录:
观察结果, 查询成功:
注意: 由于此时是联合查询, 定义 SQL 时, 额外接收了一个 user_info 表中的 username 字段, 因此, 我们需要在 ArticleInfo 类中多定义一个 username 属性, 以便将查询结果中的该字段映射到 Java 属性中:
注意: 由于联合查询时的 SQL 为 慢 SQL, 效率极低, 因此在实际工作中要避免使用多表联合查询!!
当查询的数据需要结合多张表时, 应先查询一个表的数据, 再根据得到的数据去查第二个表:
4.2 #{} 和 ${}
#{} 和 ${} 之间, 有三大主要区别:
- #{} 为预编译 sql, 提前预留参数位置, 自动识别参数类型, 性能更好.
- ${} 为即时 sql, 直接拼接参数.
- 由于 ${} 是直接拼接的方式, 如果参数是 String 类型, 需要手动加单引号, 因此存在 SQL 注入的安全问题. 而 #{} 可以自动识别参数类型, 无需加引号, 不存在 SQL 注入问题.
因此, 可以用 #{}, 就用 #{}!!
4.2.1 预编译 SQL, 即时 SQL
我们先将 @Select 注解中的 #{} 修改为 ${}, 观察执行结果:
观察日志发现, #{} 没有直接将参数传入 SQL 中, 而是使用占位符(?)进行占位; 而 ${} 是直接将参数拼接到了 SQL 中.
使用 ${} 传递字符串类型的参数, 继续观察效果:
我们发现, 程序报错了. 其实原因就是: #{} 是将参数传入 SQL 的占位符中, 而 ${} 是直接将参数拼接到了 SQL 中.
${} 直接将参数拼接到了 SQL 中, 由于没有给字符串类型的参数加引号, 从而导致了 SQL 语法错误:
而 #{} 可以进行自动类型转换, 自动为字符串类型的参数加引号:
因此, 如果要使用 ${} 传递字符串类型的参数, 需要对 ${} 外手动加上单引号, 以便 MySQL 识别其为一个字符串:
导致以上情况的原因, 就是因为使用 #{} 的 SQL 为预编译 SQL, 使用占位符将参数的位置提前预留好, 收到参数后放入占位符中, 可以对参数自动进行类型转换, 无需手动加引号.
而使用 ${} 的 SQL 为 即时 SQL, 采取直接拼接的方式拼接进 SQL 中, 若拼接的是字符串类型, 需要手动加引号.
此外, 由于 #{} 是预编译 SQL, 为参数进行了占位, 因此, MyBatis 会对参数之外的 SQL 进行缓存, 只需关注参数的变化, 根据传入参数的不同加载参数的变化即可, 不需加载参数之外的 SQL, 性能更高.
而 ${} 是即时 SQL, 不管 SQL 发生了什么变化, 每次都会重新加载整个 SQL, 因此性能较低.
4.2.2 ${}: 存在 SQL 注入!!
上文提到的预编译 SQL 也好, 性能高也好, 其实都不是选择 #{} 作为首选的主要原因.
选择 #{} 而不选择 ${} 主要原因是:
- #{} 不存在 SQL 注入问题, 安全.
- ${} 由于其需要手动添加引号, 导致其有 SQL 注入的安全问题.
SQL 注入: 通过操作输入的数据来修改事先定义好的SQL语句, 以达到执行代码对服务器进行攻击的方法.
由于没有对用户输入的值进行充分检查, 而 SQL 又是拼接而成, 在用户输入参数时, 在参数中添加⼀些 SQL 关键字, 达到改变 SQL 运行结果的目的, 完成恶意攻击.
${} 被 SQL 注入的示例如下:
如上图所示, 攻击人利用 ${} 拼接参数的漏洞, 操作参数信息, 编写 1 = '1' 恒为 true 的 SQL, 导致所有人的信息都被泄露.
SQL 注入常用于登录界面, 在不知道用户密码的情况下, 利用 SQL 注入, 实现成功登录.
而如果我们使用的是 #{} 进行参数传递, 那么 MyBatis 就会将攻击人编写的所有内容当做参数传入 SQL 中, 不会改变原本 SQL 的结构:
注意:
并非使用 ${} 就出现 SQL 注入问题, 还要结合代码分析, 如果代码进行了处理, 也是可以避免 SQL 注入的.
比如上例, 根据用户名和密码查询用户信息时, 我们可以设置查询结果只能是一条记录, 不能是多条记录, 即: 将返回值 List<UserInfo> 改为 UserInfo.
4.3 排序功能
上文说到, 为了避免 SQL 注入, 在能使用 #{} 的情况下, 尽量使用 #{}.
但是, 在传入某些特定参数时, 必须使用 ${} 才能完成相关操作, 比如: 排序功能.
我们先来看查询排序时正确 SQL :
接下来, 通过 MyBatis 进行排序查询操作.
当使用 #{} 来传递排序规则的参数(desc/asc)时, 由于在 Java 中, 参数只能通过 String 类型来传入, 所以 MyBatis 会自动给 #{} 中的参数添加单引号(desc => 'desc'), 而正确的 SQL 中此处不应该有引号, 因此导致 SQL 语法错误:
而 ${} 采用直接拼接参数的方式, 不会给参数加引号, 所以, 当实现排序功能时, 必须使用 ${} 进行参数传递:
虽然, 使用 ${} 可能具有 SQL 注入的风险, 但是我们可以通过代码进行校验, 在参数进入 Dao 层之前, 将 SQL 注入的风险排除掉即可. 如上例: 由于排序规则的参数只有 desc 和 asc 两个, 所以我们可以将传入的参数设为枚举类型, 对参数的值进行限制.
4.4 like 模糊查询
先来回顾下 like 查询的语法:
除了排序查询不能使用 #{} 外, like 查询也不能使用 #{} 来传递模糊参数. 如上图, 模糊参数为 "186", 不能将 % 传入到参数中:
此时, ${} 也能够完成:
但是, 由于此时模糊参数的值是没有限制的, 不能通过设置枚举来校验, 因此, 我们不使用 ${} 来传递模糊参数, 而是使用 MySQL 的内置函数 --- CONCAT(), 进行字符串拼接.
因此, 通过 MyBatis , 在注解中使用 concat 进行字符串拼接, 进行模糊查询:
不仅排序查询和模糊查询传参时不能使用 #{}, 如果传入的是一些非字符串类型的参数(String 类型的参数到 SQL 中, 会自动加上单引号), 也不能使用 #{} 进行传递, 例如: 列名, 表名.
5. 数据库连接池
数据库连接池 负责分配、管理和释放数据库连接, 它允许应用程序重复使用一个现有的数据库连接,
而不是再重新建立一个.
当需要与数据库建立连接时, 直接从连接池获取 Connection 对象即可, 当执行完毕后, 再将 Connection 放回连接池. 而不需像 jdbc 中频繁的获取和释放连接, 造成资源的消耗.
因此, 数据库连接池的优点如下:
- 减少了网络开销
- 资源重用
- 提升了系统的性能
常见的数据库连接池如下:
- C3P0
- DBCP
- Druid (流行)
- Hikari (流行)
SpringBoot 默认使用的数据库连接池是 Hikari:
Hikari是日语 "光" 的意思(ひかり), 以追求性能极致为目标.
Hikari 和 Druid 的性能不分伯仲.
如果想更换数据库连接池为 Druid, 引入 Druid 依赖即可:
// SpringBoot 3.x 版本
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.21</version>
</dependency>
// SpringBoot 2.x 版本
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
6. MySQL 开发规范
- 表名采用蛇形形式命名, 单词一律小写.(MySQL 在 Windows 下不区分大小写, 但在 Linux 下默认区分大小写, 因此一律采用小写)
- select 时, 不使用 * 作为查询列表字段
- 表必备三字段: id, create_time, update_time
END