【Java Web】-> MyBatis ->映射文件详解

Ⅰ 前言

MyBatis 第一课 中,我详细讲解了写一个 MyBatis 程序的全过程和它的全局配置文件。

在 MyBatis 中还有一个重要的配置文件就是 SQL 映射文件。

我们可以看一下官方文档对这个文件的描述。
在这里插入图片描述
文档中说,SQL 映射文件是 MyBatis 真正的力量所在……这里发生了神奇的事情。

这篇文章我们就来研究一下 SQL 映射文件。

Ⅱ 增、删、改操作

先回顾一下 MyBatis 第一课 中我的 case 。

我定义了一个实体类 Employee,和我的数据库中的一个表相对应;定义了一个接口 EmployeeMapper ,这个接口中定义了一个根据 id 获得 Employee 对象的方法。

在这里插入图片描述
现在我们再加上增删改的方法。
在这里插入图片描述
现在我们在 SQL 映射文件中增加新添加的 增、删、改的 SQL 语句。

在这里插入图片描述

注意这里的每个模块的 parameterType 属性都可以不写。

我们写一个测试方法,使用方法在 MyBatis 第一课 中已经详细讲述,这里不再赘述。

我们先来测试一下添加数据。
在这里插入图片描述
这里获取 SqlSession 对象我使用的是 openSession() 的无参方法,实际上还有另外一个方法。
在这里插入图片描述
此布尔值表示的是是否自动提交事务,无参的方法默认的是 false,所以我们需要在执行完写操作之后通过 commit() 手动提交一下。

数据库原始数据只有一条:
在这里插入图片描述
测试成功:
在这里插入图片描述
再来看修改。
在这里插入图片描述
测试成功
在这里插入图片描述
最后再来测试一下删除:
在这里插入图片描述
仍是成功的。
在这里插入图片描述
这里我们定义的增删改都没有返回值,MyBatis 一共支持三种类型的返回值:intlongboolean,这三种类型的基本类型或者它们的包装类都可以。

intlong 的返回值都会返回此次操作影响的行数,boolean 是在影响行数大于 0 行的时候就返回为 true

我们用增加方法来测试一下,我直接将它的返回值设置为 boolean,除此之外不需要做任何配置了,直接在接口中修改返回值即可。
在这里插入图片描述
测试结果如下
在这里插入图片描述
如果你使用的是自增主键,比如 id 值,不用自己写,数据库默认加一条记录 id 就增加 1,那我们要怎么获取数据库自增主键的值呢?

这里我们可以在 insert 标签下增加两个属性。

在这里插入图片描述
useGeneratedKeys 表示使用自增主键获取主键值策略,keyProperty 指定对应的主属性,也就是 MyBatis 在获取到主键值之后,将这个值封装给 javabean 的哪个属性。

Ⅲ 参数处理

在 MyBatis 中,我们配置 SQL 映射文件时是没有操心参数的类型问题的。
在这里插入图片描述
不管接口中传什么参数,在 SQL 映射文件中都可以根据 #{} 取出对应的值,现在我们来看一下 MyBatis 是如何进行参数处理的。

对于单个参数而言,MyBatis 不会做特殊的处理,直接可以取出参数值。比如我们要取得 id,可以在大括号里写个 id。
在这里插入图片描述

实际上,大括号里写什么都可以,因为这个大括号里只有一个参数值要取出来,所以无所谓。

比如我们写一个 yzh。
在这里插入图片描述
同样可以取出来。
在这里插入图片描述
那我们再试一下多个参数的,比如我再定义一个根据 id 和 firstName 共同查询的方法。
在这里插入图片描述
然后我们写 SQL 映射文件中的内容。
在这里插入图片描述
由于获取 SqlSessionFactory 的代码是一样的,所以我直接将它提取出来写了一个方法。
在这里插入图片描述
然后我们来看一下测试代码:
在这里插入图片描述
运行后发现报错了
在这里插入图片描述
报错说没有找到 id,可用的参数是 [arg1, arg0, param1, param2]

org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]

所以显然多个参数就需要做特殊处理了,不能直接通过 #{} 来获取参数值。

在 MyBatis 中,多个参数将会被封装到一个散列表 map 中, #{} 就是从 map 中获取指定的 key 值。

在这个 map 中,key 值就是异常中的 param1param2…,第几个参数就是 param几。而 value 才是我们要获取的参数值。

所以我们的 SQL 语句应该改成下面这样:
在这里插入图片描述
然后还需要和之前的查询一样,我们要定义返回值类型。
在这里插入图片描述
测试结果如下,正常取出了对应的数据
在这里插入图片描述
这样虽然正常取得了参数值,但是显然如果参数多了,都这样写 param1… 可读性非常差,远不如我们和取单参一样写参数的名称。

所以这里也可以使用 命名参数,明确指定封装参数时 map 中的 key。

在这里插入图片描述
只需要在接口方法的定义里写 @Param 注解即可。

现在在 SQL 中,我们就可以根据在注解中写的参数名称来获取对应的参数值了。

在这里插入图片描述
测试结果如下,是成功的。

在这里插入图片描述
/**
在我的一个中间件项目中,我完成C/S模式的框架,在其中我也用到了这个方法来实现分发器,大家有兴趣可以看一看。

【Java项目整理】-> CsFramework -> 从零开始手动搭建一个C/S模式中间件
*/

除了上面提到的方法,在处理参数的时候我们还有两个方法。

如果多个参数正好是我们业务逻辑的数据模型,那我们可以直接传入 POJO,然后取出传入的 POJO 的属性值。
在这里插入图片描述
通过 get 方法将传入的 POJO 的 id 和 firstName 取出,然后做查询。

那么如果多个参数不是业务模型中的数据,没有对应的 POJO,为了方便我们也可以传入 Map。
在这里插入图片描述
取数据的时候,可以直接使用 #{} 来取,{} 中填的就是键。我们来写一下对应的 SQL。

在这里插入图片描述
和我们用注解是一样的。

那在测试代码中,我们就需要建立于一个 Map,然后将参数值注入。
在这里插入图片描述
如果多个参数不是业务模型中的数据,但是经常要使用,我们就可以编写一个 TO(Transfer Object)数据传输对象,将这些经常使用的数据封装到一个类中。

我们再来看一个方法。

在这里插入图片描述
对于这种的单参,我们要获取其中一个 id ,是不可以直接用 #{ids[1]} 来获取的,Collection 类型和 数组类型,MyBatis 也会将它们封装在一个 Map 中,它们对应的键并不是 param0,而是特殊的键。

如果是 Collection 类型,键就是 collection,具体到 List,就是 listSet 就是 set,而数组的键是 array

我用数组来举个例子。
在这里插入图片描述

那对应的 SQL 就要写成下面这样:
在这里插入图片描述
测试成功
在这里插入图片描述

最后再做一个补充,#{}${} 取值有什么区别。

比如在这条查询中,我们用 ${} 来取 id,用 #{} 取 firstName。我们来看一这条语句的执行结果。

在这里插入图片描述
在日志中可以看到这样的显示:
在这里插入图片描述

id = 1 , first_name = ?

我们可以得到结论 #{}${} 都可以获得 Map 中的值或者 POJO 属性的值,区别是:

#{} 是以预编译的形式,将参数设置到 SQL 语句中,防止 SQL 注入;

${} 会将取出的值直接拼接在 SQL 语句中,会有安全问题。

大多数情况下,我们取参数的值都应该使用 #{} ,在原生JDBC不支持占位符的地方我们可以使用${} 进行取值。

举个例子,有一个工资的数据库,有很多年份表,比如:2018_salary,2019_salary,2020_salary。

现在我们想根据用户传进来的参数 year 先取得某一个年份表,在这个年份表中进行查询操作。

如果使用 #{} 的话就是:

select * from #{year}_salary where ...

预编译之后这个语句就变成下面这样:

select * from ?_salary where ...

这样是会报错的。

所以我们可以直接使用 ${} 进行 SQL 拼接。

select * from ${year}_salary where ...

比如 year 是2020, 转换后的结果就是:

select * from 2020_salary where ...

还是那我们之前的那张表来看,我们将表名作为参数传进去。

在这里插入图片描述

可以看到测试出错了,显示 SQL 语句有问题,因为在预编译的时候没有找到表,只得到了一个 ?

在这里插入图片描述
当我们使用 ${} 的时候,测试就通过了。
在这里插入图片描述

Ⅳ 查询

A. 返回 List

在之前的查询操作中,我们返回的都是一个对象,但是有时候我们查询需要返回的是一个 List 之类的集合,比如下面这个方法,模糊查询对应的记录。
在这里插入图片描述

我们先来写 SQL 映射。
在这里插入图片描述

注意,虽然我们最后要得到的是一个 List<Employee>,但是这里的返回类型可不是 List,而是 Employee

我们测试一下,比如要得到名字里带 e 的雇员。

在这里插入图片描述

B. 返回 Map

比如我们要返回一条记录的 Map,键为列名,值为列对应的值。
在这里插入图片描述

然后我们写 SQL,这里注意返回值就是 map 了。

在这里插入图片描述
测试结果如下:
在这里插入图片描述
这样查出来的是一条记录,现在我们来看一下返回值是多条记录的 Map,键为 id,值为 Employee。
在这里插入图片描述
还是一样,我们返回值类型设为 map。
在这里插入图片描述
那么这里就有一个问题了,我们希望返回的是 Map<Integer, Employee>,也就是以 id 作为键。但是同样的,我们也可以以名字作为键,那么返回的就是Map<String, Employee>,所以 MyBatis 要怎么得知我们需要返回的是什么类型的呢?

这就还是需要注解来实现。

在这里插入图片描述
使用 @MapKey 注解标明返回的 Map 应该以哪个属性为键。

测试结果如下:
在这里插入图片描述

C. resultMap

a. 自定义结果映射规则

在我的数据库表中,有一个字段名称是 first_name,而实体类中映射的属性是 firstName

在这里插入图片描述
之所以我们在之前的查询中,可以直接将 first_name 的值赋值给实体类的 firstName,是因为我们开启了驼峰式命名的自动映射。
在这里插入图片描述
将其设置为 false 的时候,这两个属性就没法映射了,firstName 将无法被注入值。

在这里插入图片描述
那么除了开启驼峰式命名的映射以外,我们也可以自定义结果映射规则 ,通过 resultMap 配置。

在之前的 SQL 模块中,我们使用的是 resultType 属性,现在我们换成resultMap属性。

在这里插入图片描述
由于 resultMap是自定义结果映射规则,所以我们需要先配置一个自己的映射规则。
在这里插入图片描述
注意这里是 resultMap 标签,不是resultMap 属性。resultMap 是配置自定义规则的,在这个标签下有两个必须要写的属性,id 和 type。其中 id 是这个映射规则的唯一标识,type 是要要定义的返回值类型。

在这里插入图片描述
接着我们就需要配置数据库的列名和实体类的属性之间的映射关系了。
在这里插入图片描述
其中 id 用来封装主键的映射,其他的普通列用 result 封装。如果用 id 来封装普通列也是可以的,只不过用 id 的将会被 MyBatis 标为主键,底层会有所优化。

在普通列中,其实我们也只需要数据库字段名和实体类属性名不相同的一个映射,但是这里建议只要自定义了结果映射规则,就把全部的映射规则都写上,这样会很方便对其进行修改和维护。

然后在 resultMap 属性中我们就可以引用上面自定义的规则了。

在这里插入图片描述
在关闭驼峰式命名的映射规则前提下,我们来做一个测试。
在这里插入图片描述
测试成功,说明我们自定义的规则是有效的。

b. 关联查询

① 环境准备

我们的 Employee 实体类中,我们再增加一个属性 Department 部门。

首先先建立一个部门类,它有两个属性,一个是部门的 id,一个是部门的名称。我们完成一下对应的 setter 方法以及构造方法。

package com.tyz.bean;

/**
 * @author tyz
 */
public class Department {
    
    
    private int deptId;
    private String deptName;

    public Department() {
    
    
    }

    public Department(int departmentId, String departmentName) {
    
    
        this.deptId = departmentId;
        this.deptName = departmentName;
    }

    public void setDeptId(int deptId) {
    
    
        this.deptId = deptId;
    }

    public void setDeptName(String deptName) {
    
    
        this.deptName = deptName;
    }

    @Override
    public String toString() {
    
    
        return "Department{" +
                "departmentId=" + deptId +
                ", departmentName='" + deptName + '\'' +
                '}';
    }
}


然后在 Employee 类中增加这个属性,并不足相应的 set 方法、构造方法以及 toString 方法。
在这里插入图片描述
在数据库中,我们需要创建一个部门表,并填入两条记录。
在这里插入图片描述
然后在 tbl_employee 表中增加一个部门编号的字段,并建立外键。
在这里插入图片描述

② 级联属性封装结果

现在每个员工 Employee 都对应一个部门 Department,那么我们怎么在查询
Employee的同时查询他对应的部门 Department呢?

这还是要用到 resultMap 属性的自定义映射规则。

在这里插入图片描述
前面几个属性的定义是没有变化的,因为要两个表联合查询,查出来的 dept_id 和 dept_name 值我们需要赋值给 Employee 类中的 Department 属性。要怎么赋值呢?就是我圈起来的,我们直接使用 . 表示级联属性。

department 是 Department类在Employee 类中的名称。
在这里插入图片描述
定义好映射以后我们就可以写 SQL 语句了。
在这里插入图片描述
测试通过:

在这里插入图片描述
这就是使用级联属性封装结果集的联合查询。

③ 定义关联对象封装规则

除了上面的使用级联属性封装结果集的方法以外,我们还可以使用 association 标签指定联合的 javabean 对象。

这个标签有两个属性,property 表示哪个属性是联合的对象,javaType 表示需要联合的对象类型。

在这里插入图片描述
在我们的例子中,联合的属性是 department,而 Employee 类联合的对象就是 Department类。

接着在 association 标签下,我们需要配置它和数据库字段的映射关系。

在这里插入图片描述

那么这个 returnMap 的定义我们就完成了。
在这里插入图片描述
我们来做一个测试。

在这里插入图片描述
这就是第二种联合查询的方法,使用association 标签指定联合的 javabean 对象。

④ association 分布查询 & 延迟加载

一般来说,我们如果有 Department 这张表,一定有对应的 DepartmentMapper 来操作这张表。就和我们的 Employee 一样。

在这里插入图片描述
所以我们先来建一个接口DepartmentMapper ,以及对应的配置文件。
在这里插入图片描述
在接口中我们需要定义一个查询部门的方法。
在这里插入图片描述
然后我们在 SQL 映射文件中写相关是查询语句。
在这里插入图片描述
这样我们就有了一个根据 dept_id 得到 Department 的方法。

现在我们来分步做关联查询,首先根据 id 得到 Employee,然后再根据 dept_id 得到对应的 Department

先在 EmployeeMapperPlus 接口中定义一个简单的方法,也是根据 id 查询 Employee,这个查询是做分步查询时要用的。
在这里插入图片描述
现在我们写一下分步查询 SQL。还是需要自定义一下 returnMap

在这里插入图片描述
这里的 association 就不再写返回对象类型了,因为我们需要进行分步查询。

DepartmentMapper.xml 文件中,我们写了一个 Department 的查询。
在这里插入图片描述
在分步查询这里我们就需要调用这个方法来查询 Department类。

怎么调用呢?在 association 标签中,还有一个属性 select
在这里插入图片描述
在这个属性中,我们需要把查询 Department 方法的 namespace + id 写上去。
在这里插入图片描述
select 表明当前属性是调用其指定方法查出的结果。

Employee 的查询中,我们会得到一个属性值 dept_id,我们需要将它传递给查询 Department 的方法。

那么,我们还需要在 association 中指定要将哪一列的值传递给这个查询方法。
在这里插入图片描述
所以整体这一部分分步查询的方法如下:
在这里插入图片描述
这个流程总结下来就是:使用 select 指定的方法(传入 column 指定的这列参数的值)查出对象,并封装给 property 指定的属性。

测试结果如下,测试通过。
在这里插入图片描述
分步查询还有一个更厉害的地方,就是可以使用延迟加载

什么叫延迟加载呢?就是我们在查询员工信息的时候,不需要同时也查询部门的信息,部门的信息可以等我们用的时候再进行查询。

在分段查询的基础之上,加两个配置,就可以完成延迟加载。

这个配置需要写到全局配置文件的 settings 标签下。在这里插入图片描述

lazyLoadingEnabled 如果为 true,那么关联的属性将会被延迟加载,这个属性默认是 false的,所以我们需要将其写成true

aggressiveLazyLoading 如果为true,那么一个延迟加载的对象中,当需要任何一个属性的时候,所有的属性都会被完整的加载,否则每一个属性都只在需要的时候才被加载。
在这里插入图片描述
我们来做一个简单的测试,用分步查询只取 Employee 的名字。
在这里插入图片描述
可以看到只执行了一条 SQL 语句。

现在将延迟加载的两个配置注释掉,我们再来运行一次。
在这里插入图片描述
可以看到执行了两条 SQL。这就可以很清晰地看到延迟加载的好处了,当我们不需要关联的属性时,就不会再去执行对它的查询,而是等用的时候再加载。

那我们再开启一下延迟加载,然后获取部门信息测试一下效果。
在这里插入图片描述
可以看到先输出了我们获取的名字,然后再进行了部门信息的查询,最后输出了部门信息。

⑤ 定义关联集合封装规则

我们再给 Department 增加一个属性,就是这个部门里的所有员工。
在这里插入图片描述
为了清晰起见我们再定义一个方法,查询包括员工列表在内的所有部门属性。
在这里插入图片描述
由于我们是要得到对应的雇员信息 Employee 类,所以这个查询 Department 也是一个两个表的联合查询。所以我们需要写一个联合查询的 SQL。
在这里插入图片描述

我们主要来看的是自定义结果集映射规则 resultMap 的配置。
在这里插入图片描述
剩下要写的属性就都是 Employee 的了,我们现在要把这些属性封装到 List 中。这时候我们就需要另一个标签 collection
在这里插入图片描述
collection 有两个属性, property 表示要封装的元素,那我们这里封装的就是 employees
在这里插入图片描述
ofType 表示这个集合封装的元素类型,即 Employee。在 collection 中的封装就和之前 Employee 相同了。
在这里插入图片描述
那么最终的测试结果如下,可以看到 List 正确地输出了。
在这里插入图片描述
这就是使用 collection 嵌套结果集的方式,定义关联的集合类型元素的封装规则。

⑥ collection 分步查询 & 延迟加载

既然前面集合类型的关联元素也是联合查询,我们也可以将其写成分步查询的方式。
在这里插入图片描述
查询的语句比较简单,我们主要看一下怎么配置 resultMap
在这里插入图片描述
在配置列表元素的时候,依旧需要使用 collection 标签,但是我们可以看到,
collection 标签也有一个 select 元素。
在这里插入图片描述

那么我们就需要写一个员工的查询方法,将其写到这里的 select中。
在这里插入图片描述
在这里插入图片描述
现在我们再将这个方法写到 select 中,写的是SQL的 namespace + id.

Employee 是根据部门编号做查询的,所以我们需要将部门编号传给这个方法。

在这里插入图片描述
分步查询的配置如下:
在这里插入图片描述
测试成功。
在这里插入图片描述
并且,我们可以看到,它也是执行了两条 SQL 语句,因为我们在之前的配置中已经开启了延迟加载。

在这里插入图片描述
这里还有个元素,就是可能你其他的查询都需要延迟加载,但是有的查询不需要,那我们就可以在查询中单独配置。
在这里插入图片描述
fetchType 默认是 lazy,也就是延迟加载,我们可以将其改为 eager,这样即使全局配置了延迟加载,这个分步查询也还是立即加载的。

⑦ discriminator 鉴别器

最后我们再来看一下 resultMap 中的另一个标签,discriminator 鉴别器。

MyBatis 可以使用 discriminator 判断某列的值,然后根据某列的值改变封装行为。

比如我们现在查询 Employee 类,如果查出的是女生,我们就把部门信息显示出来,如果是男生,就把 first_name这一列的值赋值给 email

我们先写出基本的属性映射。
在这里插入图片描述
然后配置鉴别器。
在这里插入图片描述
鉴别器要写两个属性,javaType 是要鉴别的列的类型,column 是要鉴别的列。

按照前面的要求,我们要根据 Employee 的性别 gender 来做不同的操作。这里注意,虽然我们写的 genderchar 类型,但是这里是没有 char 类型的映射的,所以我们写 string 是可以的。

在这里插入图片描述
在这里插入图片描述
如果是女生,我们就继续查询部门,如果是男生,我们就不对部门信息进行查询,并且将男生的 email 改成他的 first_name

在这里插入图片描述
最后我们将 Employee 的分步查询方法的结果映射设置成上面我们定义的这个。在这里插入图片描述
我们来做测试。
在这里插入图片描述
先来对男生进行查询。
在这里插入图片描述

可以看到,email 字段已经被设置成了 first_name,并且查询到的部门信息为 null

我们再来看一下女生。
在这里插入图片描述
女生按要求正常查询到了部门信息。

这就是鉴别器的作用。

猜你喜欢

转载自blog.csdn.net/qq_45627684/article/details/114272814