修复:启用评论组件

This commit is contained in:
2022-08-13 23:30:56 +08:00
parent 6a3c5bbdd6
commit abd80c6b77
135 changed files with 2234 additions and 128 deletions

View File

@@ -0,0 +1,255 @@
---
title: 快速入门
author: 查尔斯
date: 2020/12/25 14:49
categories:
- MyBatis快速入门
tags:
- MyBatis
- ORM框架
---
# 快速入门
我们将通过一个简单的 Demo 来阐述 MyBatis 的强大功能,在此之前,笔者假设你已经:
- 拥有 Java 开发环境以及相应 IDE本 Demo 采用 Eclipse 作为IDE
- 熟悉 Java Web 开发流程
- 熟悉至少一个关系型数据库(本 Demo 采用 MySQL 作为数据库)
## 数据库准备
现有一张 `User` 表,其表结构如下:
| 主键 | 姓名 | 年龄 | 邮箱 |
| :--: | :----: | :--: | :------------: |
| 1 | Jone | 18 | Jone@126.com |
| 2 | Jack | 20 | Jack@126.com |
| 3 | Tom | 28 | Tom@126.com |
| 4 | Sandy | 21 | Sandy@126.com |
| 5 | Billie | 24 | Billie@126.com |
其对应的数据库 结构 脚本如下:
```sql
-- 创建并切换数据库
CREATE DATABASE mybatis_demo_db;
USE mybatis_demo_db;
-- 创建用户数据表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名',
`age` int(11) NULL DEFAULT NULL COMMENT '年龄',
`email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Compact;
```
其对应的数据库 数据 脚本如下:
```sql
-- 清空用户表数据
TRUNCATE TABLE user;
-- 向用户表插入测试数据
INSERT INTO `user` VALUES (1, 'Jone', 18, 'Jone@126.com');
INSERT INTO `user` VALUES (2, 'Jack', 20, 'Jack@126.com');
INSERT INTO `user` VALUES (3, 'Tom', 28, 'Tom@126.com');
INSERT INTO `user` VALUES (4, 'Sandy', 21, 'Sandy@126.com');
INSERT INTO `user` VALUES (5, 'Billie', 24, 'Billie@126.com');
```
## 下载依赖
要使用 MyBatis 框架,第一步就是下载好 MyBatis 的 jar 包,我们可以从 [MyBatis](https://github.com/mybatis/mybatis-3/releases) 在 GitHub 上的开源地址下载。
![202012252222518](../../../public/img/2020/12/25/202012252222518.png)
笔者下载了 MyBatis 的核心压缩包mybatis-x.x.x.zip及其源码包mybatis-x-mybatis-x.x.x.zip
![202012252222738](../../../public/img/2020/12/25/202012252222738.png)
解压开 **mybatis-3.5.6.zip** 压缩包,目录结构如下:
![202012252222812](../../../public/img/2020/12/25/202012252222812.png)
::: tip 笔者说
如果 GitHub 下载太慢,可以前往 [FastGit](https://hub.fastgit.org/mybatis/mybatis-3/releases/tag/mybatis-3.5.6) 进行下载,它是 GitHub 的镜像地址,网站界面等各方面与 GitHub 几乎一模一样。
但是注意它仅仅是一个镜像网站,可以用于克隆或下载 GitHub 资源,但登录之类的功能是不可用的。
:::
## 创建项目
下载好依赖之后,我们通过 Eclipse创建一个动态 Web 项目,并将刚才下载的 jar 包和指定数据库驱动包添加到 WebConent\WEB-INF\lib 目录,效果如下:
![202012252223067](../../../public/img/2020/12/25/202012252223067.png)
::: tip 笔者说
本次我们不会使用到 Servlet API所以创建一个普通 Java 工程也没问题。
:::
## 创建POJO类
在 DAO 模式开发中,第一步就是要创建实体类,而在 MyBatis 项目中,实体类"弱化"为了 POJO这种类型是专门用于和数据库做映射的 Java 类型,数据表中的列与 POJO 类型的属性一 一对应。
```java
package com.example.pojo;
/**
* 用户POJO类它是Java和关系数据库表映射的类型
* @author Charles7c
*/
public class User {
private Long id;
private String name;
private Integer age;
private String email;
// 省略getter/setter方法
// 省略toString方法
}
```
::: tip 笔者说
POJOPlain Old Java Objects普通老式 Java 对象)。一般来讲,将 POJO 简单理解为实体类也无伤大雅。
:::
## 创建SQL映射文件
在 DAO 模式开发中,实体类创建完之后就是要编写 BaseDao、以及不同实体的 Dao 接口和 Dao 实现类。但这一切的繁琐过程,在现在都被 MyBatis 解决了。
现在,我们只需要按照 MyBatis 的要求创建好一个编写 SQL 的映射文件,在映射文件中编写好数据库的 CRUD 操作即可。
```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">
<!-- namespace命名空间在同一个项目中保持唯一 -->
<mapper namespace="userMapper">
<!-- SQL 操作(根据 CRUD 的不同选择不同的标签编写 SQL 操作)
idSQL 操作标识
resultType 结果集类型(全类名)
下方设定等价于编写了一个方法List<User> selectList();
-->
<select id="selectList" resultType="com.example.pojo.User">
SELECT * FROM `user`
</select>
</mapper>
```
::: tip 笔者说
SQL 映射文件的命名风格为POJO类名Mapper.xml就像命名以前的 Dao接口 一样,你可以将 SQL 映射文件理解为是以前的 Dao 实现类。
:::
## 创建核心配置文件
MyBatis 为我们简化了非常多的操作,但是一些必须由我们自定义的配置还是少不了的。在 classpath 下创建一个核心配置文件命名为:`mybatis-config.xml`
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 环境配置:
可以配置多个,但生效的只有一个
default属性指定生效的环境的id
-->
<environments default="develop">
<!-- 单个环境配置 -->
<environment id="develop">
<!-- 事务管理配置 -->
<transactionManager type="JDBC"/>
<!-- 数据源配置 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo_db"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 指定要加载的SQL映射文件 -->
<mappers>
<!-- 注意:该地址不是全类名!!! -->
<mapper resource="com/example/mapper/UserMapper.xml"/>
</mappers>
</configuration>
```
## 添加日志配置文件
在 MyBatis 中,采用的日志框架是 log4j所以为了能够查看到日志输出我们需要在 classpath 下添加一个 log4j.properties 文件。
```
###############################################################
# 输出到控制台 #
###############################################################
# log4j.rootLogger日志输出类别和级别只输出不低于该级别的日志信息DEBUG < INFO < WARN < ERROR < FATAL
# DEBUG日志级别 CONSOLE输出位置自己定义的一个名字
log4j.rootLogger=DEBUG,CONSOLE
# 配置CONSOLE输出到控制台
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
# 配置CONSOLE设置为自定义布局模式
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
# 配置CONSOLE日志的输出格式 [demo] 2019-08-22 22:52:12,000 %r耗费毫秒数 %p日志的优先级 %t线程名 %C所属类名通常为全类名 %L代码中的行号 %x线程相关联的NDC %m日志 %n换行
log4j.appender.CONSOLE.layout.ConversionPattern=[demo] %d{yyyy-MM-dd HH:mm:ss,SSS} - %-4r %-5p [%t] %C:%L %x - %m%n
```
## 测试
当一切准备好之后,完整的项目目录结构如下:
![202012252223708](../../../public/img/2020/12/25/202012252223708.png)
创建好一个单元测试类,测试一下:
```java
class TestMyBatis {
@Test
void testSelectList() throws IOException {
// 1.从classpath加载核心配置文件构建SqlSession工厂对象
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
// 2.获取SqlSession对象
try (SqlSession sqlSession = sqlSessionFactory.openSession()){
// 3.执行SQL语句 根据要执行的SQL语句选择合适的API
// p1SQL语句唯一地址 (SQL映射文件的namespace值.SQL语句的id值)
List<User> userList = sqlSession.selectList("userMapper.selectList");
// 4.遍历数据
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```
User [id=1, name=Jone, age=18, email=Jone@126.com]
User [id=2, name=Jack, age=20, email=Jack@126.com]
User [id=3, name=Tom, age=28, email=Tom@126.com]
User [id=4, name=Sandy, age=21, email=Sandy@126.com]
User [id=5, name=Billie, age=24, email=Billie@126.com]
```
## 后记
**C** 好了,与 MyBatis 的第一次约会结束了。怎么样?约会体验如何?使用步骤是不是还挺简单的?
虽然是在学习一个新技术,但是一定要时刻想想当初 DAO 模式你是怎么一个开发步骤,这样对比着会发现 MyBatis 就是在简化、优化原来的每个环节而已。根本上还是那么回事,多想想,脑子里就能留下使用思路。
::: info 笔者说
对于技术的学习,笔者一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。
所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。
:::

View File

@@ -0,0 +1,185 @@
---
title: 核心对象
author: 查尔斯
date: 2020/12/25 20:02
categories:
- MyBatis快速入门
tags:
- MyBatis
- ORM框架
---
# 核心对象
## 前言
**C** 在上一篇,笔者带大家对 MyBatis 做了一个快速入门,不知道你是否已经掌握了 MyBatis 的使用步骤呢?本篇,笔者将继续带你学习 MyBatis掌握对上篇中三个核心对象的概念理解和使用。
在 API 使用层面MyBatis 的核心类型有三个分别是SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession。
![202012252243280](../../../public/img/2020/12/25/202012252243280.png)
## SqlSessionFactoryBuilder
SqlSessionFactoryBuilder 是 MyBatis 中应用构建者模式的一个类,它的作用就是用来 **读取 MyBatis 的核心配置文件信息,然后来构建一个 SqlSessionFactory 对象**
对于 SqlSessionFactoryBuilder 这个类,使用完就没有什么价值了,所以它的生命周期只存在于方法体内即可,也就是作为一个局部变量存在。
**以下是它的使用方式:**
```java
// Resources 类是 MyBatis 中的一个资源加载工具类
// 1.从 classpath 下将 MyBatis 核心配置文件内容加载为一个输入流对象
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");   
// 2.构建 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
```
**以下是它的常用方法:**
- build(InputStream inputStream) : SqlSessionFactory 通过字节输入流构建
- build(Reader reader) : SqlSessionFactory 通过字符输入流构建
- build(Configuration config) : SqlSessionFactory 通过Configuration构建
- ....
::: tip 笔者说
因为 SqlSessionFactory 对象构建需要的配置参数很多且不能保证每个参数都是正确的或者不能一次性得到构建所需的所有参数有些参数是初始化时需要的如数据源配置延迟加载配置事务配置等有些是框架运行过程中需要的如SQL映射等[1]),所以不能采用简单的 new 方式来创建对象。
:::
## SqlSessionFactory
SqlSessionFactory 对象比较重要,它的作用是 **创建 SqlSession 对象**
**以下是它的常用方法:**
- openSession() : SqlSession 获取 SqlSession对象
该方法是我们最常用的方法,同时也是开启事务处理的(参见下方源码)
- openSession(boolean autoCommit) : SqlSession 获取SqlSession对象自行指定是否开启事务
autoCommit 参数是用来指定是否开启自动提交的
如果 autoCommit 的值为 true代表关闭事务处理。反之代表开启事务处理。
```java
// DefaultSqlSessionFactory 是 SqlSessionFactory的实现类
public class DefaultSqlSessionFactory implements SqlSessionFactory {
// ...
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
@Override
public SqlSession openSession(boolean autoCommit) {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
}
// ...
}
```
每一个 MyBatis 的应用程序都以一个 SqlSessionFactory 对象为核心。SqlSessionFactory 一旦被创建, **它的生命周期应该与应用的生命周期相同** ,所以在应用运行期间不要重复创建 SqlSessionFactory 对象。
为此,我们可以使用单例模式来改造获取 SqlSessionFactory 对象的方式。
```java
/**
* MyBatis 工具类
* @author Charles7c
*/
public class MyBatisUtils {
// 私有化构造函数
private MyBatisUtils() {}
// 静态对象
private static volatile SqlSessionFactory SQL_SESSION_FACTORY = null;
/**
* 获取 SqlSessionFactory 对象
* @return 单例的 SqlSessionFactory 对象
* @throws IOException /
*/
public static SqlSessionFactory getSqlSessionFactory() throws IOException {
if (SQL_SESSION_FACTORY == null) {
// 同步锁
synchronized(MyBatisUtils.class) {
// 双重检测机制
if (SQL_SESSION_FACTORY == null) {
// 从 classpath 加载核心配置文件,构建 SqlSession 工厂对象
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SQL_SESSION_FACTORY = new SqlSessionFactoryBuilder().build(is);
}
}
}
return SQL_SESSION_FACTORY;
}
}
```
## SqlSession
SqlSession 是 MyBatis 执行持久化操作的关键对象,类似于 JDBC 中的 Connection。 **SqlSession 对象包含了执行 SQL 所需的所有方法** ,它的底层封装了 JDBC 连接,可以用 SqlSession 实例来直接执行被映射的 SQL 语句。
**以下是它的常用方法:**
- insert(String statement, Object parameter) : int 增加操作
- delete(String statement, Object parameter) : int 删除操作
- update(String statement, Object parameter) : int 修改操作
- selectOne(String statement) : T 单个查询
- selectOne(String statement, Object parameter) : T 带参数单个查询
- selectList(String statement) : List\<E> 集合查询
- selectList(String statement, Object parameter) : List\<E> 带参数集合查询
- commit() : void 提交事务
- rollback() : void 回滚事务
- getMapper(Class\<T> type) : T 获取Mapper接口Mapper接口开发
- ...
每个线程都应该有它自己的 SqlSession 实例, SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的HttpSession。如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession返回一个响应后就关闭它。[2] **使用完 SqlSeesion 之后一定记得关闭,可以采用 finally 块或 try-with-resources** 。在 SqlSession 里可以执行多次 SQL 语句,但一旦关闭了 SqlSession 就需要重新创建。
结合 SqlSessionFactory 的获取方式改造,下方是在 MyBatisUtils 工具类中又增加了 SqlSession 的获取方式改造。
```java
/**
* 获取 SqlSession 对象(开启事务处理)
* @return SqlSession 对象
* @throws IOException /
*/
public static SqlSession openSession() throws IOException {
return getSqlSessionFactory().openSession();
}
```
## 参考文献
[1]哲雪君!. 第五篇 mybatis的运行原理2构建者模式 SqlSessionFactoryBuilder类解析[EB/OL]. https://www.cnblogs.com/zhexuejun/p/11285206.html. 2019-08-14
[2]MyBatis官网. MyBatis 入门 | 作用域Scope和生命周期[EB/OL]. https://mybatis.org/mybatis-3/zh/getting-started.html. 2020-12-26
## 后记
到此为止,笔者就介绍完了在使用 MyBatis 时,所遇到的三个核心对象。同样经过上述对这些核心对象使用的改造后,我们也看一下到底它优化到了什么程度,开开眼吧。
```java
@Test
void testSelectList() throws IOException {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()){
// 执行SQL语句
List<User> userList = sqlSession.selectList("userMapper.selectList");
// 遍历数据
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
```
::: info 笔者说
对于技术的学习,笔者一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。
所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。
:::

View File

@@ -0,0 +1,484 @@
---
title: 核心配置文件
author: 查尔斯
date: 2020/12/26 14:48
categories:
- MyBatis快速入门
tags:
- MyBatis
- ORM框架
---
# 核心配置文件
## 前言
**C** 在上一篇,笔者带大家对 MyBatis 的核心对象做了介绍。本篇,笔者将继续带你学习 MyBatis掌握对核心配置文件的使用。
MyBatis 的核心/全局配置文件 mybatis-config.xml ,顾名思义就是对 MyBatis 系统的核心设置文件。包含有 MyBatis 运行时行为配置、类型别名配置、环境配置等。
下方是核心配置文件的标签模板,笔者将对其中常用的一些标签的常用使用方式进行介绍。
configuration 根节点
- **properties** 属性配置
- settings 运行时行为配置
- **typeAliases** 类型别名配置
- typeHandlers 类型处理器
- objectFactory 对象工厂
- plugins 插件配置
- **environments** 环境配置
- environment 单个环境配置
- transactionManager 事务管理器配置
- dataSource 数据源配置
- databaseIdProvider 数据库厂商标识
- **mappers** 映射器配置
::: tip 笔者说
这些标签在使用时一定要注意标签的顺序和允许使用次数。Eclipse 中可以通过在标签上按 F2 查看该标签下的内容模型,即标签的顺序和允许使用次数。你看下图中画圈处就是各个标签的顺序,后面的 ?号 代表指定标签最多允许使用一次。
:::
![202012262251170](../../../public/img/2020/12/26/202012262251170.png)
## properties元素
如果你学过 Maven那 properties 元素应该不难理解。在 MyBatis 的核心配置文件中,有很多配置是可能经常需要变动或复用的,如果直接将值硬编码在对应位置,将不利于统一维护管理和复用。
properties 元素的作用就体现出来了,它的使用方式有两种。
### 内部编写
**第一种使用方式,是内部编写配置** ,示例如下:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 要配置哪些属性是根据你项目需求来定的,笔者仅仅做一个示例而已 -->
<properties>
<property name="mysql.driver" value="com.mysql.jdbc.Driver"/>
<property name="mysql.url" value="jdbc:mysql://localhost:3306/mybatis_demo_db"/>
<property name="mysql.username" value="root"/>
<property name="mysql.password" value="root"/>
<property name="txType" value="JDBC"/>
</properties>
<!-- ...略... -->
<environments default="develop">
<!-- 单个环境配置 -->
<environment id="develop">
<!-- 事务管理配置 -->
<transactionManager type="${txType}"/>
<!-- 数据源配置 -->
<dataSource type="POOLED">
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<!-- ...略... -->
</configuration>
```
### 外部引入
**第二种使用方式,是在外部配置文件编写配置,然后通过 properties 引入外部配置** ,示例如下:
在 classpath 下 添加 一个 properties 配置文件,记录各种配置信息。(此处笔者记录的是数据源信息)
```
# MySQL
mysql.driver=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql://localhost:3306/mybatis_demo_db
mysql.username=root
mysql.password=root
```
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 加载外部的配置文件 -->
<properties resource="db.properties"/>
<!-- ...略... -->
<environments default="develop">
<!-- 单个环境配置 -->
<environment id="develop">
<!-- 事务管理配置 -->
<transactionManager type="JDBC"/>
<!-- 数据源配置 -->
<dataSource type="POOLED">
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<!-- ...略... -->
</configuration>
```
::: tip 笔者说
大家猜一下,如果这两种方式在同时使用时遇到了相同配置,那么哪种方式的配置会生效呢?
测试思路: 可以先故意改错内部配置方式的 mysql.password 值,如果测试运行正常,说明外部配置生效了,反之则内部配置生效了。 也可以再故意调错外部的值试试。
:::
```xml
<properties resource="db.properties">
<property name="mysql.password" value="root"/>
<property name="txType" value="JDBC"/>
</properties>
```
## settings元素(下篇讲解)
settings 元素是用来设置和改变 MyBatis 在运行时的一些行为的。
| **设置项** | **描述** | **允许值** | **默认值** |
| ------------------- | :----------------------------------------------------------: | :-------------------: | :--------: |
| cacheEnabled | 对在此配置文件下的所有cache进行全局性开/关设置 | true \| false | true |
| lazyLoadingEnabled | 全局性设置懒加载。如果设为false则所有相关联的都会被初始化加载 | true \| false | true |
| autoMappingBehavior | MyBatis对于resultMap自动映射匹配级别 | NONE\|PARTIAL \|FULL | PARTIAL |
| **……(9个)** | **......** | **......** | **......** |
## typeAlias元素
在 SQL 映射文件中,我们在使用到某些类型时,需要编写好对应的全类名,大量的使用时,繁琐不说还容易错,如下 resultType 属性示例。
```xml
<select id="selectList" resultType="com.example.pojo.User">
SELECT * FROM `user`
</select>
```
而typeAlias 元素就可以解决此问题,通过它的配置,可以为指定类型配置好别名,这样在 SQL 映射文件中就可以不用写全限定类名,而是直接使用配置的类型别名了。它的使用方式也有两种。
### 单个配置
**第一种使用方式:挨个对不同类型进行别名配置。**
```xml
<typeAliases>
<!--
单个类型的别名配置(如果类型多,需要配置大量的该标签)
type指定类型的全类名
alias该类型的别名
-->
<typeAlias type="com.example.pojo.User" alias="User"/>
</typeAliases>
```
### 包扫描
**第二种使用方式:当要配置别名的类型都在指定的 package 下时,可以直接开启包扫描,批量实现别名自动配置。**
```xml
<!-- 类型别名配置 -->
<typeAliases>
<!--
会自动对指定包(包含其子包)下的类进行别名注册
注册的别名是:该类型的类名小写
例如User类自动注册的别名是user
-->
<package name="com.example.pojo"/>
</typeAliases>
```
### 使用效果
下方是有了类型别名配置之后SQL 映射文件内使用类型的效果。
```xml
<!-- 有了别名配置,使用指定类型时,直接用它的别名即可,而且不区分大小写(原因见下方) -->
<select id="selectList" resultType="User">
SELECT * FROM `user`
</select>
```
::: tip 笔者说
在 MyBatis 中有一个类 TypeAliasRegistry 它的作用就是进行类型别名注册和解析Java 中常见的类型都已经被它注册好了别名。
:::
```java
package org.apache.ibatis.type;
// ...略...
public class TypeAliasRegistry {
/** Map<类型别名, 对应类型的Class对象> */
private final Map<String, Class<?>> typeAliases = new HashMap<>();
/** 在创建对象时进行常用 Java 类型的别名注册 */
public TypeAliasRegistry() {
// String 类型注册的别名为 string
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class);
registerAlias("_byte", byte.class);
registerAlias("_long", long.class);
registerAlias("_short", short.class);
registerAlias("_int", int.class);
registerAlias("_integer", int.class);
registerAlias("_double", double.class);
registerAlias("_float", float.class);
registerAlias("_boolean", boolean.class);
registerAlias("_byte[]", byte[].class);
registerAlias("_long[]", long[].class);
registerAlias("_short[]", short[].class);
registerAlias("_int[]", int[].class);
registerAlias("_integer[]", int[].class);
registerAlias("_double[]", double[].class);
registerAlias("_float[]", float[].class);
registerAlias("_boolean[]", boolean[].class);
registerAlias("date", Date.class);
registerAlias("decimal", BigDecimal.class);
registerAlias("bigdecimal", BigDecimal.class);
registerAlias("biginteger", BigInteger.class);
registerAlias("object", Object.class);
registerAlias("date[]", Date[].class);
registerAlias("decimal[]", BigDecimal[].class);
registerAlias("bigdecimal[]", BigDecimal[].class);
registerAlias("biginteger[]", BigInteger[].class);
registerAlias("object[]", Object[].class);
registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
registerAlias("list", List.class);
registerAlias("arraylist", ArrayList.class);
registerAlias("collection", Collection.class);
registerAlias("iterator", Iterator.class);
registerAlias("ResultSet", ResultSet.class);
}
/**
* 解析别名
* @param string 要解析的别名
* @return 该别名对应的类型的Class对象
*/
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
// MyBatis 在【别名自动配置】和【解析映射文件中别名】时,对别名进行了小写转换。
// 所以在使用别名的时候才不区分大小写。
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
if (typeAliases.containsKey(key)) {
value = (Class<T>) typeAliases.get(key);
} else {
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
// ...略...
}
```
## environments元素
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中,现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。
**不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。**
所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单:
- **每个数据库对应一个 SqlSessionFactory 实例** [1]
```xml
<!-- 环境配置:
可以配置多个,但生效的只能有一个
default属性指定生效的环境的id
-->
<environments default="develop">
<!-- 单个环境配置 id 值须保证唯一 -->
<environment id="develop">
<!-- 事务管理配置
type属性事务管理器的类型
可选值有 JDBC 和 MANAGED
JDBC这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
MANAGED让容器来管理事务的整个生命周期
-->
<transactionManager type="JDBC"/>
<!-- 数据源配置
type属性数据源的类型
可选值有 POOLED 、UNPOOLED、JNDI
POOLED使用连接池来管理连接对象降低连接的开销
UNPOOLED不采用连接池每次都会进行连接打开和关闭
JNDI了解采用 JNDI 来获取数据源,这种方式不用配置下方的 property。
它当初出现的目的就是为了能在同一个应用服务器内的不同应用间共享数据源
-->
<dataSource type="POOLED">
<!-- 下方只是最重要的四项配置,不同类型数据源有不同额外配置 -->
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
```
## mappers元素
mappers 元素的作用就是用来告诉 MyBatis 去哪找我们编写的 SQL 语句,它的使用方式有两大类。
### 指定映射文件
这类方式主要是告诉 MyBatis 我们所编写的 SQL 映射文件的地址,我们之前在 [《快速入门》](./01-快速入门) 中使用的就是属于这类方式。它有两种实现:
```xml
<!-- 使用相对于类路径的资源引用,有多少 SQL 映射文件就写多少个 mapper 配置 -->
<mappers>
<mapper resource="com/example/mapper/UserMapper.xml"/>
</mappers>
```
```xml
<!-- 使用完全限定资源定位符URL (很少使用) -->
<mappers>
<mapper url="file:///E:/eclipse-workspace/mybatis-1/src/com/example/mapper/UserMapper.xml"/>
</mappers>
```
### 指定Mapper接口[重要]
这类方式还要涉及到 SqlSession 对象的另一个使用形式:**Mapper接口开发**
我们之前使用 SqlSession 是让它来直接执行指定的 SQL 语句。这种方式需要指明 SQL 映射文件 namespace的名字以及 SQL 语句的 id。因为是硬编码在代码中维护时有诸多不便例如容易写错不说还不利于我们在持久层采用面向接口编程思想。
```java
// 执行 SQL 语句
List<User> userList = sqlSession.selectList("userMapper.selectList");
```
而 Mapper 接口开发就可以有效解决此问题,实现方式如下:
**第一步:先创建一个 Mapper 接口。** 前期就养成一个开发习惯,保持 Mapper 接口和对应 SQL 映射文件同名同包(虽然目前不同名也没事,但是先听话,养成习惯)。
```java
public interface UserMapper {
/**
* 查询用户列表
* @return
*/
List<User> selectList();
}
```
![202012262252221](../../../public/img/2020/12/26/202012262252221.png)
**第二步:将 SQL 映射文件的 namespace 值改为对应 Mapper 接口的全限定类名。**
```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="com.example.mapper.UserMapper">
</mapper>
```
**第三步:将 SQL 映射文件中的 SQL 语句和 Mapper 接口中的方法进行绑定。**
```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="com.example.mapper.UserMapper">
<!-- 与 Mapper 接口的方法绑定
id 就是 Mapper 接口中的对应方法名
resultType 就是 Mapper 接口中的返回值类型,如果返回值类型时集合,则写它的泛型
-->
<select id="selectList" resultType="User">
SELECT * FROM `user`
</select>
</mapper>
```
**最后,我们在核心配置文件中,再配置好 Mapper 接口的全限定类名即可。**
```xml
<!-- 使用 Mapper 接口的完全限定类名 -->
<mappers>
<mapper class="com.example.mapper.UserMapper"/>
</mappers>
```
**测试一下。**
```java
@Test
void testSelectList() throws IOException {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()){
// 获取 Mapper 接口,而不再直接执行 SQL 语句
// 和以前 DAO 模式就非常相像了,只不过是 DAO 实现类变为了 SQL 映射文件
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行方法
List<User> userList = userMapper.selectList();
// 遍历数据
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
```
另外,在核心配置文件中,大量的配置 Mapper 接口全限定类名,还是有些麻烦,所以 MyBatis 在这也支持包扫描配置。
```xml
<!-- 包扫描,将指定包(及其子包)下的 Mapper 接口全部注册为映射器 -->
<mappers>
<package name="com.example.mapper"/>
</mappers>
```
::: warning 笔者说
Mapper 接口开发是我们以后主要使用的方式,必须掌握!
:::
## 参考文献
[1]MyBatis 官网. MyBatis 配置[EB/OL]. https://mybatis.org/mybatis-3/zh/configuration.html. 2020-12-26
## 后记
::: info 笔者说
对于技术的学习,笔者一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。
所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。
:::

View File

@@ -0,0 +1,519 @@
---
title: SQL映射文件之查询元素
author: 查尔斯
date: 2020/12/27 23:35
categories:
- MyBatis快速入门
tags:
- MyBatis
- ORM框架
---
# SQL映射文件之查询元素
## 前言
**C** 在上一篇,笔者带大家对 MyBatis 的核心配置文件做了介绍。本篇开始,笔者将带你学习 MyBatis 的 SQL 映射文件,它是 MyBatis 中亮点最多的部分(翻回去看看 MyBatis 特点,主要优势都在这儿),同时也是未来我们使用 MyBatis 开发时接触最多的部分。
不过你也别担心MyBatis 在 SQL 语句映射方面异常强大,但 SQL 映射文件却是相当简单。
下方是 SQL 映射文件的标签模板,笔者将花两三篇对其中常用的一些标签的常用使用方式进行介绍。
**mapper** 根标签
- cache-ref 引用其它命名空间的缓存配置
- **cache** 配置给定命名空间的缓存
- ***resultMap*** (自定义结果集映射配置)用来描述数据库结果集和对象的对应关系,是最复杂也是最强大的元素
- ~~parameterMap~~ (自定义参数映射配置)此元素已被废弃,并可能在将来被移除!请使用行内参数映射。
- **sql** 可以重用的 SQL 块
- **insert** 映射插入语句
- **update** 映射更新语句
- **delete** 映射删除语句
- **select** 映射查询语句
![202012271107691](../../../public/img/2020/12/27/202012271107691.png)
## mapper元素
mapper 元素是 SQL 映射文件的根标签,在该标签内有一个属性 namespace命名空间可以理解为当前 SQL 映射文件的标识。
**传统 SqlSession 开发中** mapper 元素的 namespace 属性和下方子元素的 id 属性联合保证了 SQL 语句的唯一标识。
```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="userMapper">
<select id="selectList" resultType="User">
SELECT * FROM `user`
</select>
</mapper>
```
```java
List<User> userList = sqlSession.selectList("userMapper.selectList");
```
**SqlSession 的 Mapper 接口开发中** mapper 元素的 namespace 属性必须命名为对应的 Mapper 接口的全限定类名,下方子元素也要和对应 Mapper 接口中的方法一 一对应。
```java
package com.example.mapper;
// ...略...
public interface UserMapper {
/**
* 查询用户列表
* @return /
*/
List<User> selectList();
}
```
```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="com.example.mapper.UserMapper">
<!-- List<User> selectList(); -->
<select id="selectList" resultType="User">
SELECT * FROM `user`
</select>
</mapper>
```
::: tip 笔者说
Mapper 接口开发是我们上篇中最后部分讲解过的 SqlSession 使用方式以后也是主要的写法很好理解GKD掌握。
:::
## select元素
在每一个项目中,查询都是应用最频繁也是应用最困难的部分。 在 SQL 映射文件中select 元素就是用于编写查询 SQL 的,它是 MyBatis 中最常用的元素之一。
select 元素有很多属性,可以很详细的来配置每条语句的行为细节。
- **id** 命名空间中唯一的标识符
Mapper 接口开发中id 值需要和接口中对应方法的名字保持一致
- **parameterType** 传入SQL语句的参数类型
可以为参数类型的全限定类名或别名
Mapper接口开发中parameterType 值需要和接口中对应方法的参数类型保持一致
- **resultType** SQL语句返回值类型详细解释见 resultMap 元素部分)
可以为返回值类型的全限定类名或别名
Mapper接口开发中resultType 值需要和接口中对应方法的返回值类型保持一致
**注意:** 如果返回值类型是集合,那么 resultType 值应该表示为集合的泛型类型,而不是集合类型。
接下来,笔者通过几个示例来带大家掌握下 select 元素的使用。
::: tip 笔者说
笔者只是介绍了使用最为频繁的几个属性,如果想了解更多的属性含义,可以前往[官网](https://mybatis.org/mybatis-3/zh/sqlmap-xml.html)查看。
:::
### 用户名查询
在《快速入门》篇的数据库基础上,我们先来实现一个根据用户名的模糊查询。
首先,在 Mapper 接口中我们添加一个方法。
```java
public interface UserMapper {
/**
* 根据用户名模糊查询
* @param name 用户名
* @return 用户列表
*/
List<User> selectByName(String name);
}
```
然后我们在 SQL 映射文件中再添加一个与该方法对应的查询元素。
```xml
<!-- List<User> selectByName(String name); -->
<!-- 通过#{参数名}即可获取传入的值 -->
<select id="selectByName" parameterType="string" resultType="User">
select * from user where name like concat('%', #{name}, '%')
</select>
```
测试一下:
```java
class TestMyBatis {
@Test
void testSelectByName() throws IOException {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
List<User> userList = userMapper.selectByName("J");
// 遍历数据
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
select * from user where name like concat('%', ?, '%')
```
```
User [id=1, name=Jone, age=18, email=Jone@126.com]
User [id=2, name=Jack, age=20, email=Jack@126.com]
```
#### #{}和${}的区别
在控制台输出的 MyBatis 日志中,我们可以看到最后执行的 SQL 就是我们在传统 JDBC 开发中,为了解决 SQL 注入攻击而编写的 SQL 形式。
之所以输出如此,是因为我们使用了 #{} 的形式来使用参数,\#{}表示一个占位符号,可以接收简单类型值或 POJO 属性值,通过 #{} 可以实现 preparedStatement 向占位符中设置值,自动进行 Java 类型和 JDBC 类型转换。#{} 可以有效防止 SQL 注入。
**注意:** #{} 占位符不能放置在字符串中select * from user where name like '%#{name}%' 是错误的。
---
其实在 MyBatis 中还有占位符:${},但是基本不使用,至于原因,我们试试看就知道了。
```xml
<!-- List<User> selectByName(String name); -->
<!-- ${参数名} 可以直接放在字符串中 -->
<select id="selectByName" parameterType="string" resultType="User">
select * from user where name like '%${name}%'
</select>
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
select * from user where name like '%J%'
```
这回知道原因了吧? **总结一下它们的区别(面试题)**
- 在使用`#{}`参数语法时MyBatis 会创建 `PreparedStatement` 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)[1]
- 在使用 `${}`MyBatis 会将 SQL 中的 `${}` 替换成对应变量的值。适合需要直接插入一个不转义的字符串时使用。
- 使用 #{} 可以有效的防止 SQL 注入,提高系统安全性。
### 多参数查询
我们也都知道,在 Java 中定义方法的时候,返回值类型只能设定为一个具体类型,但是方法的参数是可以定义 N 个的那么在面对这种方法时MyBatis 查询元素的 parameterType 该如何使用呢?
其实也非常简单,**有三种方式比较流行**
- 将多个参数封装到 POJO / 自定义对象中
- 将多个参数封装到 Map / List 集合中
- 将多个参数通过 @Param 注解标注
我们通过一个案例感受下不同方式的区别: **案例需求:根据用户名、年龄查询用户列表**
#### 封装到POJO
首先,在 Mapper 接口中我们添加一个方法。
```java
public interface UserMapper {
/**
* 根据用户名和年龄查询
* @param user 用户信息
* @return 用户列表
*/
List<User> selectByUser(User user);
}
```
然后我们在 SQL 映射文件中再添加一个与该方法对应的查询元素。
```xml
<!-- List<User> selectByUser(User user); -->
<!-- 在 parameterType 为对象时, #{属性名} 可以获取对象中的属性值 -->
<select id="selectByUser" parameterType="User" resultType="User">
select
*
from
user
where
name like concat('%', #{name}, '%')
and age = #{age}
</select>
```
测试一下:
```java
class TestMyBatis {
@Test
void testSelectByUser() throws IOException {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
User user = new User();
user.setName("J");
user.setAge(20);
List<User> userList = userMapper.selectByUser(user);
// 遍历数据
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
select * from user where name like concat('%', ?, '%') and age = ?
```
```
User [id=2, name=Jack, age=20, email=Jack@126.com]
```
#### 封装到Map集合
::: tip 笔者说
笔者个人比较喜欢这一种Map 集合总是那么"万金油"。
:::
首先,在 Mapper 接口中我们添加一个方法。
```java
public interface UserMapper {
/**
* 根据用户名和年龄查询
* @param params 条件参数
* @return 用户列表
*/
List<User> selectByMap(Map<String, Object> params);
}
```
然后我们在 SQL 映射文件中再添加一个与该方法对应的查询元素。
```xml
<!-- List<User> selectByMap(Map<String, Object> params); -->
<!-- 在 parameterType 为 Map 集合时, #{map的键名} 可以获取集合的值 -->
<select id="selectByMap" parameterType="map" resultType="User">
select
*
from
user
where
name like concat('%', #{name}, '%')
and age = #{age}
</select>
```
测试一下:
```java
class TestMyBatis {
@Test
void testSelectByMap() throws IOException {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
Map<String, Object> params = new HashMap<>();
params.put("name", "J");
params.put("age", 20);
List<User> userList = userMapper.selectByMap(params);
// 遍历数据
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
select * from user where name like concat('%', ?, '%') and age = ?
```
```
User [id=2, name=Jack, age=20, email=Jack@126.com]
```
#### @Param注解
::: tip 笔者说
@Param 注解与上述两种方式有些不同,如果你使用了该注解,那么 parameterType 就不需要再手动指定了,使用 **普通类型参数** Java中的 int、double、String...... 这些都属于普通类型参数,而对象和集合就不属于普通类型参数了)的方法一般都推荐使用它。
:::
首先,在 Mapper 接口中我们添加一个方法。
```java
public interface UserMapper {
/**
* 根据用户名和年龄查询
* @param name 用户名
* @param age 年龄
* @return 用户列表
*/
// @Param("参数名") 注解中传入的参数名才是占位符要使用到的名字
List<User> selectByParam(@Param("name") String name, @Param("age") Integer age);
}
```
然后我们在 SQL 映射文件中再添加一个与该方法对应的查询元素。
```xml
<!-- List<User> selectByParam(@Param("name") String name, @Param("age") Integer age); -->
<!-- #{@Param注解设定的名} 可以用来取出对应参数的值 -->
<select id="selectByParam" resultType="User">
select
*
from
user
where
name like concat('%', #{name}, '%')
and age = #{age}
</select>
```
测试一下:
```java
class TestMyBatis {
@Test
void testSelectByParam() throws IOException {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
List<User> userList = userMapper.selectByParam("j", 20);
// 遍历数据
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
select * from user where name like concat('%', ?, '%') and age = ?
```
```
User [id=2, name=Jack, age=20, email=Jack@126.com]
```
::: warning 使用注意
超过 3 个以上的 **普通类型参数** 最好封装成对象或 Map 来入参(特别是在常规的增加和修改操作时,字段较多,封装成对象比较方便),而参数固定的业务方法,最好使用 @Param 来入参(这种方法比较灵活,代码的可读性高,可以清晰看出来这个接口方法的所需的参数是什么,并且对于固定的接口方法,参数一般是固定的,所以可以使用直接参数入参即可,无需封装对象。例如:修改个人密码的方法,根据 id 删除用户的方法,根据 id 查看用户明细的方法,都可以采取这种方式)
:::
## sql元素
在同一个 SQL 映射文件中,经常面临着重复 SQL 的问题,尤其是查询类 SQL 。效果如下:
```xml
<select id="selectByName" resultType="User">
select
id, name, age, email
from
user
where name like concat('%', #{name}, '%')
</select>
```
```xml
<select id="selectById" resultType="User">
select
id, name, age, email
from
user
where id = #{id}
</select>
```
使用 sql 元素可以让我们得以复用一些 SQL语句。
```xml
<!-- 定义可重用 SQL 段 -->
<sql id="selectUser">
select
id, name, age, email
from
user
</sql>
<select id="selectByName" resultType="User">
<!-- 引用 SQL -->
<include refid="selectUser"/>
where name like concat('%', #{name}, '%')
</select>
<select id="selectById" resultType="User">
<include refid="selectUser"/>
where id = #{id}
</select>
```
## 参考文献
[1]MyBatis 官网. XML 映射文件[EB/OL]. https://mybatis.org/mybatis-3/zh/sqlmap-xml.html. 2020-12-26
## 后记
本篇中select 元素是重点,笔者列了好多个示例,你一定要将示例代码完整"临摹" + "思考"一遍,这样才能达到笔者所说的技术学习的第二步、第三步。
::: info 笔者说
对于技术的学习,笔者一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。
所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。
:::

View File

@@ -0,0 +1,342 @@
---
title: SQL映射文件之增删改元素
author: 查尔斯
date: 2020/12/27 23:55
categories:
- MyBatis快速入门
tags:
- MyBatis
- ORM框架
---
# SQL映射文件之增删改元素
## 前言
**C** 在上一篇,笔者带大家对 MyBatis SQL 映射文件的 select 元素、sql 元素进行了学习。本篇,笔者将带你学习 MyBatis SQL 映射文件中的 insert、update、delete元素这三个可以说是 SQL 映射文件中最为简单的,别愣神,快跟上我。
![202012271125273](../../../public/img/2020/12/27/202012271125273.jpg)
## insert元素
**案例需求新增用户PeiQi18PeiQi@126.com**
首先,在 Mapper 接口中我们添加一个方法。
```java
public interface UserMapper {
/**
* 添加用户
* @param user 用户信息
* @return 影响行数
*/
int insert(User user);
}
```
::: tip 笔者说
insert、update、delete 这类操作,本身默认就是返回影响的行数,所以不需要对 resultType 进行指定。在定义这类接口方法的时候设置返回值类型为 int 即可。
:::
然后我们在 SQL 映射文件中再添加一个与该方法对应的查询元素。
```xml
<!-- int insert(User user); -->
<insert id="insert" parameterType="User">
insert into user (name, age, email) values(#{name}, #{age}, #{email})
</insert>
```
测试一下:
```java
class TestMyBatis {
@Test
void testInsert() {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
User user = new User();
user.setName("PeiQi");
user.setAge(18);
user.setEmail("PeiQi@126.com");
int rows = userMapper.insert(user);
System.out.println("影响行数为:" + rows);
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
insert into user (name, age, email) values(?, ?, ?)
```
```
影响行数为1
```
---
输出的结果一如既往的表示成功了,但是当你前往数据库查看时,却并没有新增数据。
![202012271125276](../../../public/img/2020/12/27/202012271125276.png)
仔细看看执行日志吧!相比于查询元素,插入元素执行多了一行日志,大白话理解就是:**JDBC 连接正在回滚事务** 。
![202012271126381](../../../public/img/2020/12/27/202012271126381.png)
这是因为我们在获取 SqlSession 的时候,采用的是开启事务的方式。开启事务对于查询没什么大影响,但是对于增删改,如果你不提交事务,就意味着不会将数据持久化到数据库。
所以改动一下测试代码吧:
```java
class TestMyBatis {
@Test
void testInsert() {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
User user = new User();
user.setName("PeiQi");
user.setAge(18);
user.setEmail("PeiQi@126.com");
int rows = userMapper.insert(user);
System.out.println("影响行数为:" + rows);
// 提交事务
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
这回测试之后就正常了。
![202012271127794](../../../public/img/2020/12/27/202012271127794.png)
### 获取自增主键值
我们在一些支持自动生成主键的数据库中设置了主键自增,当数据插入之后,我们可能需要用到刚生成的主键值,这时候传统的方法是自己去手动查询一次,而 MyBatis 则是通过在插入元素上添加几个属性就可以解决了。
```xml
<!-- int insert(User user); -->
<!--
【useGeneratedKeys:】(仅适用于 insert 和 update表示要获取自动生成的主键
这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键
(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段默认值false。
【keyProperty】:(仅适用于 insert 和 update表示获取到自动生成主键之后应该映射到对象的哪个属性中
MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值默认值未设置unset
如果生成列不止一个,可以用逗号分隔多个属性名称。 [1]
-->
<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into user (name, age, email) values(#{name}, #{age}, #{email})
</insert>
```
测试一下:
```java
class TestMyBatis {
@Test
void testInsert() {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
User user = new User();
user.setName("PeiQi");
user.setAge(18);
user.setEmail("PeiQi@126.com");
System.out.println("插入前:" + user);
int rows = userMapper.insert(user);
System.out.println("影响行数为:" + rows);
System.out.println("插入后:" + user);
// 提交事务
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```
插入前User [id=null, name=PeiQi, age=18, email=PeiQi@126.com]
影响行数为1
插入后User [id=8, name=PeiQi, age=18, email=PeiQi@126.com]
```
## update元素
再来试试 update 元素。**案例需求将id为1的用户年龄改为20。**
首先,在 Mapper 接口中我们添加一个方法。
```java
public interface UserMapper {
/**
* 修改用户
* @param user 用户信息
* @return 影响行数
*/
int update(User user);
}
```
然后我们在 SQL 映射文件中再添加一个与该方法对应的查询元素。
```xml
<!-- int update(User user); -->
<update id="update" parameterType="User">
update user set age = #{age} where id = #{id}
</update>
```
测试一下:
```java
class TestMyBatis {
@Test
void testUpdate() {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
User user = new User();
user.setId(1L);
user.setAge(20);
int rows = userMapper.update(user);
System.out.println("影响行数为:" + rows);
// 提交事务
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
update user set age = ? where id = ?
```
```
影响行数为1
```
## delete元素
再来试试 delete 元素。**案例需求将id为1的用户删除。**
首先,在 Mapper 接口中我们添加一个方法。
```java
public interface UserMapper {
/**
* 根据ID删除用户
* @param id 用户ID
* @return 影响行数
*/
int deleteById(@Param("id") Long id);
}
```
然后我们在 SQL 映射文件中再添加一个与该方法对应的查询元素。
```xml
<!-- int deleteById(@Param("id") Long id); -->
<delete id="deleteById">
delete from user where id = #{id}
</delete>
```
测试一下:
```java
class TestMyBatis {
@Test
void testDeleteById() {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
int rows = userMapper.deleteById(1L);
System.out.println("影响行数为:" + rows);
// 提交事务
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
delete from user where id = ?
```
```
影响行数为1
```
## 参考文献
[1]MyBatis 官网. XML 映射文件[EB/OL]. https://mybatis.org/mybatis-3/zh/sqlmap-xml.html. 2020-12-26
## 后记
你就看看,它们使用起来简单不?这还能有理由说学不动、学不会吗?
::: info 笔者说
对于技术的学习,笔者一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。
所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。
:::

View File

@@ -0,0 +1,397 @@
---
title: SQL映射文件之自定义映射元素
author: 查尔斯
date: 2020/12/28 00:07
categories:
- MyBatis快速入门
tags:
- MyBatis
- ORM框架
---
# SQL映射文件之自定义映射元素
## 前言
**C** 在上一篇,笔者带大家对 MyBatis SQL 映射文件的 insert、update、delete元素做了介绍。到此CRUD 的基本操作我们就介绍完了。本篇,笔者将带你学习 MyBatis SQL 映射文件的 resultMap 元素,它是 MyBatis 中号称"最强"的元素。有多强?容易令人头秃。
![202012281130648](../../../public/img/2020/12/28/202012281130648.jpg)
## resultMap元素
要介绍 resultMap 元素,那必然要先详细提一下 resultType 属性。
### resultType属性
在 select 元素中,我们一直在使用 resultType 属性,我们可以用它来指定 SQL 查询完后的 ResultSet结果集到底映射为哪种类型。
下方的示例中resultType 代表的就是将查询回来的所有结果数据映射为User类型的对象。
```xml
<select id="selectById" resultType="User">
select
id, name, age, email
from
user
where
id = #{id}
</select>
```
MyBatis 会按照 SQL 查询出的结果数据的列名或别名来映射。
![202012281131280](../../../public/img/2020/12/28/202012281131280.png)
如果列名和属性名不能匹配上,可以在 SELECT 语句中设置列别名来完成匹配,效果如下。
```xml
<select id="selectById" resultType="User">
select
id, name as username, age, email
where
id = #{id}
</select>
```
![202012281131380](../../../public/img/2020/12/28/202012281131380.png)
### 简单自定义映射
在学习了上面的知识后,你会发现上面的例子没有一个需要显式配置 `ResultMap`,这就是 `ResultMap` 的优秀之处:你完全可以不用显式地配置它们。
::: tip 笔者说
实际上 `resultType` 属性的实现原理就是 `ResultMap` MyBatis 在幕后会自动创建一个 `ResultMap`,再根据属性名来映射列到 JavaBean 的属性上。所以记得注意 **二者不能同时存在**
:::
虽然上面的例子不用显式配置 `ResultMap`,但为了讲解,我们来看看如果在刚刚的示例中,显式使用外部的 `resultMap` 会怎样,这也是解决列名不匹配的另外一种方式。[1]
```xml
<!--
自定义映射
id属性resultMap的唯一标识
type属性要映射为的Java类型全限定类名或别名
-->
<resultMap id="userMap" type="User">
<!--
用来映射主键列,可以帮助提高整体性能(建议配置)
propertyJava类型的属性名
column结果集的列名或别名
-->
<id column="id" property="id"/>
<!-- 用来映射普通列 -->
<result column="name" property="username"/>
</resultMap>
<select id="selectById" resultMap="userMap">
select
id, name, age, email
from
user
where id = #{id}
</select>
```
::: tip 笔者说
在简单自定义映射时我们仅仅需要对结果集列名和类属性名不一致的情况作映射规则指定其他一致的MyBatis 依然可以帮助我们自动映射好。
但是当出现复杂的自定义映射时MyBatis 将会进入映射"罢工"状态,未指明自定义映射规则的部分将不再进行自动映射,看看下方的例子吧。
:::
### 复杂自定义映射[难点]
除了上述简单的自定义映射元素外,在 resultMap 元素中,还有两个用于进行复杂映射的子元素(多表操作):
- **association** 映射到 JavaBean 的某个“复杂类型”属性例如JavaBean类
- **collection** 映射到 JavaBean 的某个“复杂类型”属性,例如:集合
我们通过两个案例分别体会一下这两个子元素。
#### association案例
**案例需求根据ID查询用户信息同时将该用户的角色信息也查询出来。**
刚才我们做了那么多练习,数据库搞的太乱了,**我们先重置回 MyBatis 第一篇的数据库** ,并做一些数据库上的调整。
```sql
-- 创建并初始化数据表 role
CREATE TABLE `role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Compact;
INSERT INTO `role` VALUES (1, '超级管理员');
INSERT INTO `role` VALUES (2, '普通管理员');
-- 为 user 表添加 roleId 列
ALTER TABLE `user` ADD COLUMN `roleId` bigint(0) NULL COMMENT '角色id';
-- 为 user 表做一些角色修改
UPDATE user SET roleId = 1 WHERE id BETWEEN 1 AND 3;
UPDATE user SET roleId = 2 WHERE id > 3;
```
根据数据库改动,创建及改动 POJO
```java
/**
* 角色 POJO
* @author Charles7c
*/
public class Role {
private Long id;
private String name;
// 省略 getter/setter 、toString
}
```
```java
/**
* 用户 POJO
* @author Charles7c
*/
public class User {
private Long id;
private String name;
private Integer age;
private String email;
private Long roleId;
// 角色对象属性
private Role role;
// 省略 getter/setter 、toString
}
```
改造好数据库之后,我们直接来改造一下 SQL 映射文件中的对应查询。
```xml
<resultMap id="userMap" type="User">
<id column="id" property="id"/>
<!-- 复杂的类型关联:一对一映射,映射对象属性
property对象属性在映射类中的名字
javaType对象属性的类型全限定类名或别名
-->
<association property="role" javaType="Role">
<!-- 和外层的映射一样的,"俄罗斯套娃" -->
<id column="rid" property="id"/>
<result column="rname" property="name"/>
</association>
</resultMap>
<select id="selectById" resultMap="userMap">
select
u.*,
r.id as rid,
r.name as rname
from
user u
left join role r on u.roleId = r.id
where u.id = #{id}
</select>
```
![202012281132193](../../../public/img/2020/12/28/202012281132193.png)
测试一下:
```java
class TestMyBatis {
@Test
void testSelectById() {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
User user = userMapper.selectById(2L);
System.out.println(user);
System.out.println(user.getRole());
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
select u.*, r.id as rid, r.name as rname from user u left join role r on u.roleId = r.id where u.id = ?
```
```
User [id=2, name=null, age=null, email=null]
Role [id=1, name=超级管理员]
```
---
看看这结果MyBatis 映射是不是"罢工"了?解决方法有两种:
1. 挨个的把所有结果集列与对应类属性映射好
2. 在 MyBatis 核心配置文件中,更改自动映射的默认级别
```xml
<settings>
<!-- 自动映射行为设置 -->
<setting name="autoMappingBehavior" value="FULL"/>
</settings>
```
再来试试,看看控制台输出的结果。
```
User [id=2, name=Jack, age=20, email=Jack@126.com]
Role [id=1, name=超级管理员]
```
#### collection案例
**案例需求根据ID查询用户信息同时将该用户的联系人列表也查询出来。**
我们再来做一些数据库上的调整。
```sql
-- 创建并初始化数据表 linkuser
CREATE TABLE `linkuser` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系人名',
`phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系电话',
`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系地址',
`userId` bigint(20) NULL DEFAULT NULL COMMENT '用户id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '联系人表' ROW_FORMAT = Compact;
INSERT INTO `linkuser` VALUES (1, '张三', '18822233311', '北京西城区', 2);
INSERT INTO `linkuser` VALUES (2, '李四', '18822233322', '北京东城区', 2);
```
根据数据库改动,创建及改动 POJO
```java
/**
* 联系人 POJO
*
* @author Charles7c
*/
public class LinkUser {
private Long id;
private String name;
private String phone;
private String address;
private Long userId;
// 省略 getter/setter 、toString
}
```
```java
/**
* 用户 POJO
*
* @author Charles7c
*/
public class User {
private Long id;
private String name;
private Integer age;
private String email;
private Long roleId;
// 角色对象属性
private Role role;
// 联系人列表
private List<LinkUser> linkUserList;
// 省略 getter/setter 、toString
}
```
改造好数据库之后,我们直接来改造一下 SQL 映射文件中的对应查询。
```xml
<resultMap id="userMap" type="User">
<id column="id" property="id"/>
<!-- 复杂类型集合,一对多,映射集合属性
property集合属性在映射类中的名字
ofType集合属性的泛型全限定类名或别名
-->
<collection property="linkUserList" ofType="LinkUser">
<id column="lkuid" property="id"/>
<result column="lkuname" property="name"/>
<result column="lkuphone" property="phone"/>
<result column="lkuaddress" property="address"/>
</collection>
</resultMap>
<select id="selectById" resultMap="userMap">
select
u.*,
lku.id as lkuid,
lku.name as lkuname,
lku.phone as lkuphone,
lku.address as lkuaddress
from
user u
left join linkuser lku on u.id = lku.userId
where
u.id = #{id}
</select>
```
![202012281133167](../../../public/img/2020/12/28/202012281133167.png)
测试一下:
```java
class TestMyBatis {
@Test
void testSelectById() {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
User user = userMapper.selectById(2L);
System.out.println(user);
user.getLinkUserList().forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL 语句
select u.*, lku.id as lkuid, lku.name as lkuname, lku.phone as lkuphone, lku.address as lkuaddress from user u left join linkuser lku on u.id = lku.userId where u.id = ?
```
```
User [id=2, name=Jack, age=20, email=Jack@126.com]
LinkUser [id=1, name=张三, phone=18822233311, address=北京西城区, userId=null]
LinkUser [id=2, name=李四, phone=18822233322, address=北京东城区, userId=null]
```
## 参考文献
[1]MyBatis 官网. XML 映射文件[EB/OL]. https://mybatis.org/mybatis-3/zh/sqlmap-xml.html. 2020-12-26
## 后记
大多数人在学到复杂自定义映射时都容易犯迷糊,所以笔者说过如果要学习 Hibernate 框架,开局容易深入难,因为 Hibernate 这框架中经常要处理类似的映射,年轻人慢慢来,加油!
::: info 笔者说
对于技术的学习,笔者一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。
所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。
:::

View File

@@ -0,0 +1,218 @@
---
title: SQL映射文件之缓存元素
author: 查尔斯
date: 2020/12/28 00:17
categories:
- MyBatis快速入门
tags:
- MyBatis
- ORM框架
---
# SQL映射文件之缓存元素
## 前言
**C** 在上一篇,笔者带大家对 MyBatis SQL 映射文件的 resultMap 元素做了介绍,它大概是 MyBatis 学习中第一个 "坎儿",没跨过来的同学也没关系,慢慢来,切勿急躁,先看看本篇再说。本篇,笔者将带你学习 MyBatis SQL 映射文件的 cache 元素。
cache 即缓存任何应用都不可缺少的一个组成部分但凡想提升性能缓存就得拿出来说道说道。MyBatis 中自然也少不了缓存的存在,下面我们去看看吧。
## cache元素
cache 元素,是用于开启 MyBatis 二级缓存的关键。在 MyBatis 中缓存分为一级缓存和二级缓存 。
### 一级缓存
**一级缓存主要指的是 Session 缓存,默认是开启并生效的**
一级缓存存在两种作用域范围:[2]
- `SESSION`(默认)**在同一个 SqlSession 中多次执行同一个查询,除第一次走数据库,剩下的都走缓存** 。
- `STATEMENT` 每执行完一个 Mapper 中的语句后都会将一级缓存清除(不推荐配置)。
测试一下一级缓存的 `SESSION` 作用域范围:(随便找两个查询试试就可以)
```java
class TestMyBatis {
@Test
void testSelectByList() {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行不同 SQL
List<User> userList1 = userMapper.selectList();
List<User> userList2 = userMapper.selectByName("J");
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
很显然它执行了两条 SQL和缓存根本搭不上关系。
![202012281138752](../../../public/img/2020/12/28/202012281138752.png)
再来试试执行两次相同的 SQL 查询。
```java
class TestMyBatis {
@Test
void testSelectByList() {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行相同 SQL
List<User> userList1 = userMapper.selectList();
List<User> userList2 = userMapper.selectList();
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
结果显而易见MyBatis 在同一个 SqlSession 中,对相同的 SQL 查询,只执行了一次,第二次则直接使用了缓存。
![202012281139250](../../../public/img/2020/12/28/202012281139250.png)
### 二级缓存
**二级缓存是指 mapper 映射文件。** 二级缓存的作用域是同一个 namespace 下的 mapper 映射文件内容,**多个 SqlSession 之间是共享的** 。
::: warning 笔者说
可以通过核心配置文件中的 settings 元素的 cacheEnabled 对所有二级缓存进行全局性开/关设置默认值为true
:::
```xml
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
```
在测试二级缓存前,我们需要先对指定的 SQL 映射文件启用二级缓存,即添加一个 cache 元素。
```xml
<!-- 对于同一个 SQL 映射文件来讲,只能使用一个 cache 元素 -->
<cache/>
```
上面我们仅仅添加了一个空 cache 元素 ,但其实它已经采用了很多缓存默认值,大致如下:[1]
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存(哪怕最后没提交事务也会刷新缓存)。
- 缓存会使用最近最少使用算法LRU, Least Recently Used算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
你也可以通过修改 cache 元素的属性来调整二级缓存。
```xml
<!-- 这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。 -->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
```
**eviction** 代表缓存清除策略:(默认的清除策略是 LRU
- `LRU` 最近最少使用:移除最长时间不被使用的对象
- `FIFO` 先进先出:按对象进入缓存的顺序来移除它们
- `SOFT` 软引用:基于垃圾回收器状态和软引用规则移除对象
- `WEAK` 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
**flushInterval** 代表缓存刷新间隔:(默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新)它的属性值可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。
**size** 代表可以缓存的对象引用数目:(默认值是 1024它的属性值可以被设置为任意正整数但要注意欲缓存对象的大小和运行环境中可用的内存资源。
**readOnly** 代表缓存中的对象是否只读:(默认值是 false它的属性值可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例因此这些对象不能被修改这就提供了可观的性能提升。而可读写的缓存会通过序列化返回缓存对象的拷贝速度上会慢一些但是更安全 **建议** )。
---
赶快测试一下吧:
```java
class TestMyBatis {
@Test
void testSelectByList() {
try {
// 获取SqlSession对象
SqlSession sqlSession1 = MyBatisUtils.openSession();
// 执行
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
List<User> userList1 = userMapper1.selectList();
// 【执行关闭操作,将 SqlSession 中的数据写到二级缓存区域】
sqlSession1.close();
// 获取SqlSession对象
SqlSession sqlSession2 = MyBatisUtils.openSession();
// 执行
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
List<User> userList2 = userMapper2.selectList();
sqlSession2.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
控制台报错了User 没有序列化,上方 readOnly 属性刚介绍完,它默认值为 false表示每次会通过序列化返回缓存对象的拷贝以此实现可读写的缓存。
![202012281139256](../../../public/img/2020/12/28/202012281139256.png)
```java
public class User implements Serializable{
// 略
}
```
再测试之后控制台输出如下Cache Hit Ratio 表示缓存命中率开启二级缓存后每执行一次查询系统都会计算一次二级缓存的命中率。第一次查询也是先从缓存中查询只不过缓存中一定是没有的所以命中率为0然后再从DB中查询后缓存到二级缓存中。第二次查询的时候是从二级缓存中读取的这一次的命中率为1/2=0.5。 当然若有第三次查询则命中率为1/3=0.66 ,依此类推。[3]
![202012281139540](../../../public/img/2020/12/28/202012281139540.png)
## cache-ref元素
当我们想要在多个命名空间中共享相同的缓存配置和实例时cache-ref 元素就可以派上用场了,当同时使用了 cache 元素和 cache-ref 元素时cache 元素的优先级更高。
```xml
<!-- namespace要共享二级缓存的某 SQL 映射文件的 namespace 的值 -->
<cache-ref namespace="com.example.mapper.UserMapper"/>
```
::: tip 笔者说
二级缓存也不是万能的,需要根据实际情况来,当查询操作远远多于增删改操作的情况下,并且业务对数据的实时性要求不高的时候可以采用二级缓存,否则增删改频繁刷新二级缓存将会降低系统性能,而缓存又会导致实时效果差。
而且 MyBatis 的二级缓存也存在着一些缺陷,使用 MyBatis 二级缓存必须有一个前提:保证所有的增删改查都在同一个 namespace 下才行,不然容易出现数据不一致问题,例如:当两个 SQL 映射文件中均存在对同一个表的操作,那么其中一方修改了表,只会引发该 SQL 映射文件的二级缓存清空,而不会清空另一个的。
:::
## 参考文献
[1]MyBatis 官网. XML 映射文件[EB/OL]. https://mybatis.org/mybatis-3/zh/sqlmap-xml.html. 2020-12-26
[2]花好夜猿. Mybatis【面试题】讲讲Mybatis的缓存-简答[EB/OL]. https://blog.csdn.net/qq_23202687/article/details/103708458. 2019-12-26
[3]陈浩翔. 你真的懂Mybatis缓存机制吗[EB/OL]. https://mp.weixin.qq.com/s/h2x15k71rClaHjcz7u2dlQ. 2018-07-10
## 后记
SQL 映射文件的初步学习终于结束了,幸好有之前的文章雏形,但就这样还花费了半天时间整理和完善,但愿它能给小白用户带来一份系统的学习方案。
::: info 笔者说
对于技术的学习,笔者一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。
所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。
:::

View File

@@ -0,0 +1,672 @@
---
title: 动态SQL
author: 查尔斯
date: 2020/12/29 13:28
categories:
- MyBatis快速入门
tags:
- MyBatis
- ORM框架
---
# 动态SQL
## 前言
**C** 一晃,都来到了本系列的最后一篇了,第一篇[《快速入门》](./01-快速入门) 中,我们曾烦恼的问题们(见下方代码),被 MyBatis 的强大能力逐个逐个的瓦解了。
我们还剩下没有攻克的问题,只有 **动态参数拼接** 了。但凡使用 JDBC 做过持久层开发,你应该就能理解根据不同条件拼接 SQL 语句有多痛苦,例如:拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号[1]、解决多余的 and等。
本篇我们要学习 MyBatis 的另一个强大特性:动态 SQL 元素,利用它,我们可以彻底摆脱这种痛苦。
```java
// 假设BaseDao已经封装了通用CRUD操作详情见笔者《BaseDao封装JDBC通用CRUD》
public class UserDaoImpl extends BaseDao implements UserDao {
// 根据条件查询用户列表
@Override
public List<User> findByMap(Map<String, Object> params) throws Exception {
// 动态拼接SQL语句
StringBuffer sqlBuffer = new StringBuffer();
// 动态拼接SQL占位符参数
List<Object> paramsList = new ArrayList<>();
sqlBuffer.append(" select ");
sqlBuffer.append(" * ");
sqlBuffer.append(" from ");
sqlBuffer.append(" user ");
sqlBuffer.append(" where 1 = 1 ");
// 根据用户名模糊查询
Object name = params.get("name");
if (EmptyUtils.isNotEmpty(name)) {
sqlBuffer.append(" and name like CONCAT('%',?,'%') ");
paramsList.add(name);
}
// 根据年龄查询
Integer age = (Integer) params.get("age");
if (EmptyUtils.isNotEmpty(age)) {
sqlBuffer.append(" and age = ? ");
paramsList.add(age);
}
return this.selectList(sqlBuffer.toString(), paramsList.toArray(), User.class);
}
// 保存用户
@Override
public int save(User user) throws Exception {
StringBuffer sqlBuffer = new StringBuffer();
sqlBuffer.append(" insert into user ");
sqlBuffer.append(" (name, age, email) ");
sqlBuffer.append(" values(?, ?, ?) ");
Object[] params = {user.getName(), user.getAge(), user.getEmail()};
return this.insert(sqlBuffer.toString(), params);
}
}
```
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。[1]
- ***if*** 判断元素
- choose (when, otherwise) 判断元素
- **trim (where, set)** 自动去除多余组成元素
- *foreach* 迭代元素
上方这部分介绍,主要来自 MyBatis 官网至于它怎么介chuixu自己我们不用过多关注我们自己用了还好用才是真的。
## if元素
SQL 映射文件篇的时候,我们实现过一个需求:根据用户名和年龄查询用户列表。关键实现部分如下:
```java
public interface UserMapper {
/**
* 根据用户名和年龄查询
* @param params 条件参数
* @return 用户列表
*/
List<User> selectByMap(Map<String, Object> params);
}
```
```xml
<!-- List<User> selectByMap(Map<String, Object> params); -->
<!-- 在 parameterType 为 Map 集合时, #{map的键名} 可以获取集合的值 -->
<select id="selectByMap" parameterType="map" resultType="User">
select
*
from
user
where
name like concat('%', #{name}, '%')
and age = #{age}
</select>
```
如果一切如下方的传值测试,那什么问题也不会发生。
```java
class TestMyBatis {
@Test
void testSelectByMap() throws IOException {
// 获取SqlSession对象
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行 SQL
Map<String, Object> params = new HashMap<>();
params.put("name", "J");
params.put("age", 20);
List<User> userList = userMapper.selectByMap(params);
// 遍历数据
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
但是往往我们在做类似的需求时对条件一般是可选的name 和 age 参数可以传递值也可以不传递值,那么当你如是做之后,结果呢?我们在 Map 集合中去除掉 age 这个参数值试试。
```java
// 执行
Map<String,Object> map = new HashMap<String, Object>();
map.put("name", "J");
List<User> userList = userMapper.selectByMap(params);
```
**控制台输出:**
![202012291143905](../../../public/img/2020/12/29/202012291143905.png)
我们去除了 age 参数值,原本的设想是不再根据 age 列进行条件筛选,但是它还是处于生效状态,因为我们并没有添加 SQL 的条件判断, age = null 的结果显而易见是不存在的。
---
我们来使用 if 元素改造一下吧。
```xml
<!--
if元素 用于条件判断
test属性利用关系和逻辑运算符编写条件不需要用${}括起来
注意:有些同学受到 JSTL 荼毒太深,就容易犯错 -->
<select id="selectByMap" parameterType="map" resultType="User">
select
*
from
user
where
name like concat('%', #{name},'%')
<if test="age != null">
and age = #{age}
</if>
</select>
```
执行刚才的测试代码,此次就没有任何问题了。
![202012291144689](../../../public/img/2020/12/29/202012291144689.png)
::: tip 笔者说
if 元素可以说是日常开发用的最多的了,而且又简单,又好用。
:::
## where元素
有了 if 元素的加持,如果我们只是要进行 age 这一项条件查询,那就去掉 Map 集合中的 name 参数,这时候似乎只需要在 SQL 映射文件中也给 name 条件查询加上个判断即可。
```xml
<!-- 字符串判断可以多加一个 != '' 空字符串的判断 -->
<select id="selectByMap" parameterType="map" resultType="User">
select
*
from
user
where
<if test="name != null and name != ''">
name like concat('%', #{name},'%')
</if>
<if test="age != null">
and age = #{age}
</if>
</select>
```
```java
// 执行
Map<String,Object> map = new HashMap<String, Object>();
map.put("age", 20);
List<User> userList = userMapper.selectByMap(map);
```
但结果并不是如此,当测试执行后,控制台报错了。
![202012291144692](../../../public/img/2020/12/29/202012291144692.png)
但实际上,你就算不运行,你好好看看上方的 SQL 部分就可以看出问题它一共有可能出现4种情况。
1. name 和 age 都传递了值,那一切都正常执行
2. 只传递了 name 值,那么也正常
3. *如果只传递了 age 值,那么就会多余一个 and 出现*
4. *如果都没有传值,那么就会多余一个 where 出现*
在之前 JDBC 开发中,我们的解决方法是添加一个 `where 1 = 1` 的恒等式。
```xml
<select id="selectByMap" parameterType="map" resultType="User">
select
*
from
user
where
1 = 1
<if test="name != null and name != ''">
and name like concat('%', #{name},'%')
</if>
<if test="age != null">
and age = #{age}
</if>
</select>
```
在 MyBatis 中,准备了一个专门的元素用来解决此类问题,比添加额外的 `1=1` 语句更简单。
```xml
<!--
where 元素在执行时,会智能添加一个 where 查询关键字
并且结合着 if 元素,它会自动【去除】每个 if 段内前面【多余的 and 或 or】 关键字(不会去除后面的)
-->
<select id="selectByMap" parameterType="map" resultType="User">
select
*
from
user
<where>
<if test="name != null and name != ''">
name like concat('%', #{name},'%')
</if>
<if test="age != null">
and age = #{age}
</if>
</where>
</select>
```
## set元素
set 元素,和 where 元素一样也是结合 if 元素用于去除多余内容的。顾名思义,它是和 update 语句的 set 部分有关的。
我们来做一个修改示例:
```java
public interface UserMapper {
/**
* 修改用户信息
* @param user 用户信息
* @return 影响行数
*/
int update(User user);
}
```
```xml
<!-- int update(User user); -->
<update id="update" parameterType="User">
update
user
set
name = #{name},
age = #{age},
email = #{email},
roleId = #{roleId}
where
id = #{id}
</update>
```
```java
class TestMyBatis {
@Test
void testUpdate1() {
// 获取SqlSession
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 只想修改1号用户的名字
User user = new User();
user.setName("Charles");
user.setId(1L);
// 执行
int rows = userMapper.update(user);
System.out.println("影响行数为:" + rows);
// 提交事务
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
执行测试后,本来我们只想修改用户名,结果却是将所有的列都清空了。
![202012291145084](../../../public/img/2020/12/29/202012291145084.png)
![202012291145312](../../../public/img/2020/12/29/202012291145312.png)
所以它也需要加上 if 元素来判断是否需要修改。
```xml
<update id="update" parameterType="User">
update
user
set
<if test="name != null">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="roleId != null">
roleId = #{roleId}
</if>
where
id = #{id}
</update>
```
但如果仅仅这样,当最后一个条件判断不成立时,那么前面的内容只要有一个传了值就会在结尾出现多余 `,` 的问题,使用 set 元素可以很容易解决该类问题。
```xml
<update id="update" parameterType="User">
update
user
<set>
<if test="name != null">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="nemailame != null">
email = #{email},
</if>
<if test="roleId != null">
roleId = #{roleId}
</if>
</set>
where
id = #{id}
</update>
```
set 元素可以智能添加一个 set 关键字,并且可以去除 if 段后面多余的 `,` 号。
## trim元素
当我们需要灵活变更自动去除的内容时where 和 set 就不合适了,它们是死板的,只能去除那几样。
trim 元素,看名字就知道是用来去除东西,下面我们用 trim 元素分别实现一下刚才的 where 元素和 set元素。
实现 where 元素。
```xml
<!--
trim元素用于去除多余内容
prefix为整段内容添加一个前缀
prefixOverrides自动去除每个 if 段的多余前缀内容
-->
<select id="selectByMap" parameterType="map" resultType="User">
select
*
from
user
<trim prefix="where" prefixOverrides="and | or">
<if test="name != null and name != ''">
name like concat('%', #{name},'%')
</if>
<if test="age != null">
and age = #{age}
</if>
</trim>
</select>
```
实现 set 元素。
```xml
<!--
trim元素用于去除多余内容
suffix为整段内容添加一个后缀
suffixOverrides去除每个if段后多余的内容
-->
<update id="update" parameterType="User">
update
user
<trim prefix="set" suffix="where id = #{id}" suffixOverrides=",">
<if test="name != null">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="roleId != null">
roleId = #{roleId}
</if>
</trim>
</update>
```
## foreach元素
上方的四个元素,可以说在 SQL 映射文件中,用的最多,你把它们掌握了一般情况够用了。
但是有时候我们有一些特殊的需求,例如:下方的案例,根据角色 id 的集合来查询用户列表。
```java
public interface UserMapper {
/**
* 根据角色集合查询用户
* @param roleIds 角色集合
* @return 用户列表
*/
List<User> selectByRoleList(List<Long> roleIds);
}
```
```xml
<!--
foreach 迭代元素
collection
可选值为 listList集合、array数组、Map集合的键对应的值需要为List或Array
item遍历出来的每一项
open在整段前添加的内容
separator遍历出来的每一项都添加什么分隔符
close在整段后添加的内容
-->
<select id="selectByRoleList" parameterType="long" resultType="User">
select
*
from
user
where
roleId in
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
```
测试一下:
```java
class TestMyBatis {
@Test
void selectByRoleList() {
// 获取SqlSession
try (SqlSession sqlSession = MyBatisUtils.openSession()) {
// 获取Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 执行
List<Long> roleIds = Arrays.asList(1L, 2L);
List<User> userList = userMapper.selectByRoleList(roleIds);
// 遍历
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL
select * from user where roleId in ( ? , ? )
```
```
User [id=2, name=JSON, age=20, email=Jack@126.com]
User [id=3, name=Tom, age=28, email=Tom@126.com]
User [id=4, name=Sandy, age=21, email=Sandy@126.com]
User [id=5, name=Billie, age=24, email=Billie@126.com]
User [id=8, name=PeiQi, age=18, email=PeiQi@126.com]
```
## choose元素
最后我们再介绍一个应用不算太多的元素choose它比较类似于在 Java 中的 switch 选择结构。
**案例需求:查询用户列表,如果参数 name 有值那就根据 name 模糊查询,否则参数 age 有值那就根据 age 查询, 如果都没值那就查询所有角色为1的用户。**
```java
public interface UserMapper {
/**
* 查询用户列表
* name有值时根据名字做模糊查询
* 否则age有值时根据年龄等值查询
* 都没值查询角色Id为1的用户
* @param user 查询条件
* @return 用户列表
*/
List<User> selectByUser(User user);
}
```
```xml
<select id="selectByUser" parameterType="User" resultType="User">
select
*
from
user
<where>
<choose>
<when test="name != null and name != ''">
name like concat('%', #{name},'%')
</when>
<when test="age != null">
age = #{age}
</when>
<otherwise>
roleId = 1
</otherwise>
</choose>
</where>
</select>
```
测试一下,如果同时传了 name 和 age的值。
```java
@Test
void testSelectByUser() {
try (SqlSession sqlSession = MyBatisUtils.openSession()){
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setName("T");
user.setAge(18);
List<User> userList = userMapper.selectByUser(user);
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL
select * from user WHERE name like concat('%', ?,'%')
```
测试一下,如果只传了 age 的值。
```java
@Test
void testSelectByUser() {
try (SqlSession sqlSession = MyBatisUtils.openSession()){
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setAge(18);
List<User> userList = userMapper.selectByUser(user);
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL
select * from user WHERE age = ?
```
测试一下,如果什么值都没传。
```java
@Test
void testSelectByUser() {
try (SqlSession sqlSession = MyBatisUtils.openSession()){
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.selectByUser(null);
userList.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
}
```
**控制台输出:**
```sql
-- 输出的 SQL
select * from user WHERE roleId = 1
```
::: tip 笔者说
效果很明显了,自上而下进行条件判断,只要其中一个满足,后面的条件就不再执行。这就是 switch 的机制。
:::
## 参考文献
[1]MyBatis 官网. 动态 SQL[EB/OL]. https://mybatis.org/mybatis-3/zh/dynamic-sql.html. 2020-12-28
## 后记
《初识MyBatis》 系列到本篇就结束了,笔者的更文速度,你还能追的上吗?:smile:
其实这不过是因为笔者有存稿而已,否则哪那么快,每篇文章在最初时都花费了大量的时间,在存稿基础上再来发表又花费了不短时间,所以真没什么产出速度,大概比一些"临近太监的网文"的作者快点?
这个系列结束,不代表 MyBatis 系列就完全结束了,后续笔者会再次抽时间开放 MyBatis 新的系列 《MyBatis原理》当然这可能需要一段时间因为笔者最近在忙另一个系列。但愿那时候还在关注的你依然保持有对 MyBatis 热爱、研究的心。:gift_heart:
::: info 笔者说
对于技术的学习,笔者一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。
所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。
:::