1.1 什么是 MyBatis?
MyBatis 是支持普通 SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis 消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis 使用简单的 XML或注解用于配置和原始映射,将接口和 Java 的POJOs(Plain Ordinary Java Objects,普通的 Java对象)映射成数据库中的记录。
2. MyBatis - HelloWorld
2.1 工程搭建
2.1.1 建立数据库和数据库表
DROP TABLE IF EXISTS `tbl_employee`;
CREATE TABLE `tbl_employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`last_name` varchar(45) ,
`gender` char(1),
`email` varchar(45),
PRIMARY KEY (`id`)
) ;
2.1.2 导入 jar 包
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
2.1.3 建立 bean 类 Employee.java
public class Employee {
private Integer id;
private String lastName;
private String gender;
private String email;
//getter、setter
//toString()
//有参构造、无参构造
}
2.1.4 编写 MyBatis 全局配置文件 mybatis-config.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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="EmployeeMapper.xml"></mapper>
</mappers>
</configuration>
几个玩意:
- environments:环境,比如生产环境、测试环境、部署环境等待
- default:指定标签中的某一个环境作为默认值
- environment:具体每一个环境的定义
- transactionManager:事务控制,这里写 JDBC
- dataSource:数据源
- type:类型,这里是 POOLED,连接池
- property:定义数据库的常规四个属性
- mappers:该标签里面存放多个 MyBatis 的映射配置文件
- mapper:指定每一个映射文件的所在位置
2.1.5 编写 Employee 的 SQL 映射配置文件 EmployeeMapper.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.hedon.mybatis.dao.EmployeeMapper">
<select id="getEmpById" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee where id = #{id}
</select>
</mapper>
几个玩意:
- namespace:命名空间,指定该映射文件所指向的接口的全限定路径
- select:表明这是一条 select 类的 sql 语句
- id:指定方法名,唯一,该语句映射完就是映射到一个方法上
- resultType:方法返回的类型,这里是 Employee
- select 标签之间:写 sql 语句
- #{id}:取参数,这里只有一个参数,#{id}、#{arg}、#{abc}都是一个效果的
2.1.6 编写接口 EmployeeMapper.java
public interface EmployeeMapper {
Employee getEmpById(Integer id);
}
2.1.7 测试
@Test
public void test() throws IOException {
//读取 MyBatis 全局配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
//获取 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//获取 sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try{
//反射生成 EmployeeMapper 的代理对象,这个时候 mapper 配置文件就已经映射到 getEmpById() 方法上,使其具有具体的功能了 => 接口式编程
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
//使用代理对象来执行方法
Employee empById = mapper.getEmpById(1);
System.out.println(empById);
}finally {
//关闭 sqlSession
sqlSession.close();
}
}
2.1.8 结果
# 结果如下:
Employee{id=1, lastName='null', gender=1, email='171725713@qq.com'}
# 留下的问题:
这里的 lastName 的值没有取到,原因是数据库中的字段是 last_name 而 Employee 类中的属性是 lastName。
# 注意点:
SqlSession 代表和数据库的一次会话,用完必须关闭;
SqlSession 和 connection 一样都是非线程安全的,每次使用都应该去获取新的对象;
2.2 功能架构
我们把Mybatis的功能架构分为三层:
2.2.1 API 接口层
提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
2.2.2 数据处理层
负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
2.2.3 基础支撑层
负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
2.3 总体流程
我们来根据前面的 HelloWorld 案例来梳理 MyBatis 的总体流程。
Created with Raphaël 2.2.0 开始 读取配置文件 myabtis-config.xml 从 myabtis-config.xml 中的 <mappers> 标签找到 EmployeeMapper.xml 在 EmployeeMapper.xml 中找到 getEmpById,请求生成 SQL 语句 注入 id 的值,生成完整的 SQL 语句 去数据库里面玩一圈,执行 SQL 语句 得到结果映射,并进行转换处理,得到最终的 Employee 对象 释放连接资源2.3.1 加载配置并初始化
每个 MyBatis 应用程序主要都是使用SqlSessionFactory
实例的,一个SqlSessionFactory
实例可以通过SqlSessionFactoryBuilder
获得。SqlSessionFactoryBuilder
可以从一个xml配置文件
或者一个预定义的配置类的实例
获得。
用 xml 文件构建 SqlSessionFactory 实例是非常简单的事情。推荐在这个配置中使用类路径资源(classpath resource),但你可以使用任何 Reader 实例,包括用文件路径或 file://开头的 url 创建的实例。MyBatis有一个实用类Resources
,它有很多方法,可以方便地从类路径及其它位置加载资源。
-
触发条件:加载配置文件。
-
处理过程:
将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。
2.3.2 接收调用请求
- 触发条件:调用 Mybatis 提供的API
- 传入参数:为 SQL 的 ID 和传入参数对象
- 处理过程:将请求传递给下层的请求处理层进行处理。
2.3.3 处理操作请求
- 触发条件:API 接口层传递请求过来
- 传入参数:为 SQL 的 ID 和传入参数对象
- 处理过程:
- 根据 SQL 的 ID 查找对应的 MappedStatement 对象。
- 根据传入参数对象解析 MappedStatement 对象,得到最终要执行的 SQL 和执行传入参数。
- 获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果。
- 根据 MappedStatement 对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理结果。
- 释放连接资源。
2.3.4 返回处理结果将最终的处理结果返回
3. MyBatis 全局配置文件
3.1 properties
引入外部配置文件,可以写一些固定的配置,如数据库连接等。
-
jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis jdbc.username=root jdbc.password=root
-
mybatis-config.xml
<!-- properties 有两个属性: resource:引用类路径资源 url:引用网络路径或磁盘路径资源 --> <properties resource="jdbc.properties"></properties> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments>
3.2 settings
可修改MyBatis在运行时的行为方式。
下面以 mapUnderscoreToCamelCase(开启自动驼峰命名规则)举例。
3.2.1 修改 mybatis-config.xml 文件
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
3.2.2 再次测试
# 结果:
Employee{id=1, lastName='hedon', gender=1, email='171725713@qq.com'}
# 对比:
可以发现,开启了自动驼峰命名规则后,lastName 的值就可以获取到了。
3.3 typeAliases
类型别名只是Java类型的简称。它仅与 XML 配置有关,只是为了减少完全限定的类名的冗余类型而存在。
3.3.1 单独取别名 XML 版本 —— typeAlias
3.3.1.1 修改 mybatis-config.xml
<!--
起别名,这样以后就可以用 Employee 代替 com.hedon.mybatis.bean.Employee
注意:如果以后不指定 alias 的话,默认就是类名小写 => employee
-->
<typeAliases>
<typeAlias type="com.hedon.mybatis.bean.Employee" alias="Employee"></typeAlias>
</typeAliases>
这样在 EmployeeMapper.xml 中简化 com.hedon.mybatis.bean.Employee 的写法了。
3.3.1.2 修改 EmployeeMapper.xml
<!--<select id="getById" resultType="com.hedon.mybatis.bean.Employee">-->
<select id="getById" resultType="Employee">
select * from tbl_employee where id = #{id}
</select>
3.3.2 批量取别名 —— package
我们还可以进行批量取别名,经测试,默认别名是类名(首字母大小写都可以)=> 别名不区分大小写。
3.3.2.1 修改 mybatis-config.xml
<typeAliases>
<!--
package:为某个包下的所有类批量起别名
name:指定包,为当前包及其子包的每一个类都起一个默认别名(经测试,类名和类名首字母小写都可以)。
-->
<package name="com.hedon.mybatis.bean"/>
</typeAliases>
注意:可能存在子包下也有 Employee 类,导致冲突,运行报错(因为别名已经用过了,想再用到另外一个身上,肯定不行)。
这时候可以用 @Alias 注解来进一步区分。
3.3.2.2 为子包下的 Employee 单独取名字
package com.hedon.mybatis.bean.example;
import org.apache.ibatis.type.Alias;
//这样这个类就不会在用 Employee 做别名了,就不存在冲突了。
@Alias(value = "SecondEmployee")
public class Employee {
private String text;
}
3.3.4 MyBatis 内置的别名
我们起别名的时候不要和这些重复了,会冲突。
3.4 typeHandlers
无论是 MyBatis 在预处理语句(PreparedStatement)中 设置一个参数时,还是从结果集中取出一个值时, 都会 用类型处理器将获取的值以合适的方式转换成 Java 类型。
3.5 Handling Enums
3.6 objectFactory
3.7 plugins
插件是MyBatis提供的一个非常强大的机制,我们可以通过插件来修改 MyBatis 的一些核心行为。插件通过动态代理机制,可以介入四大对象的任何一个方法的执行。
四大对象:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
3.8 environments
MyBatis可以配置多种环境,比如开发、测试和生 产环境需要有不同的配置。
每种环境使用一个environment标签进行配置并指 定唯一标识符。
可以通过environments标签中的default属性指定一个环境的标识符来快速的切换环境。
3.8.1 id
指定当前环境的唯一标识。
3.8.2 transactionManager
type:
-
JDBC —— JdbcTransactionFactory
使用了 JDBC 的提交和回滚设置,依赖于从数 据源得到的连接来管理事务范围。
-
MANAGED —— ManagedTransactionFactory
不提交或回滚一个连接、让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。
-
自定义
实现 TransactionFactory 接口,type=全类名/ 别名。
3.8.3 dataSource
type:
-
UNPOOLED —— UnpooledDataSourceFactory
不使用连接池。
-
POOLED —— PooledDataSourceFactory
使用连接池。
-
JNDI
在 EJB 或应用服务器这类容器中查找指定的数据源。
-
自定义
实现 DataSourceFactory 接口,定义数据源的获取方式。
3.9 databaseIdProvider
MyBatis 可以根据不同的数据库厂商执行不同的语句。
3.9.1 type
DB_VENDOR:
使用 MyBatis 提供的 VendorDatabaseIdProvider
解析数据库厂商标识。也可以实现 DatabaseIdProvider
接口来自定义。
3.9.2 property
-
name
数据库厂商标识。
-
value
为标识起一个别名,方便SQL语句使用 databaseId 属性引用。
3.9.2 匹配规则
1、如果没有配置databaseIdProvider标签,那么databaseId=null;
2、如果配置了databaseIdProvider标签,使用标签配置的name去匹 配数据库信息,匹配上设置databaseId=配置指定的值,否则依旧为 null;
3、如果databaseId不为null,他只会找到配置databaseId的sql语句;
4、MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的所有语句。如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。
3.10 mappers
将 sql 映射文件注册到全局配置中。
3.10.1 逐个注册
<mappers>
<!--resource 引用类路径下的 mapper 文件-->
<mapper resource = "mybatis/mapper/EmployeeMapper.xml"></mapper>
<!--url 引用网络或者磁盘路径下的 mapper 文件-->
<mapper url = "file:///D:/EmployeeMapper.xml"></mapper>
<!--
class 注册接口
1. 有 sql 映射文件,映射文件名必须和接口同名,并且放在与接口同一目录下。
2. 在接口类中的每一个方法都加上类似 @Select("sql 语句") 这样的注解,就可以不写 xml 配置文件直接注入。
-->
<mapper class = "com.hedon.mybatis.EmployeeMapper"></mapper>
<mapper class = "com.hedon.mybatis.PersonAnnotation"></mapper>
</mappers>
3.10.2 批量注册
<mappers>
<!--
将 com.hedon.mapper 包下的 *Mapper 接口类注册进来。
这种方式要求 SQL 映射文件名必须和接口名相同并且在同一目录下。
-->
<package name="com.hedon.mapper"></package>
</mappers>
4. MyBatis 映射文件
4.1 insert
4.1.1 示例
<insert id="addEmp" parameterType="com.hedon.mybatis.bean.Employee">
insert into tbl_employee(last_name,gender,email) values (#{lastName},#{gender},#{email})
</insert>
4.1.2 获取主键自增的值
<insert>
标签中有 2 个属性:
- useGeneratedKeys:默认为 false,设置为 true 的话就可以取到自增的主键;
- keyProperty:指明这个自增得到的主键要放在哪个属性上
<insert id="addEmp" parameterType="com.hedon.mybatis.bean.Employee"
useGeneratedKeys="true"
keyProperty="id">
insert into tbl_employee(last_name,gender,email) values (#{lastName},#{gender},#{email})
</insert>
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = new Employee();
employee.setLastName("AAAAABC");
employee.setGender("男");
employee.setEmail("!234");
System.out.println("添加前 ID:"+employee.getId()); //添加前 ID:null
mapper.addEmp(employee);
System.out.println("添加后 ID:"+employee.getId()); //添加后 ID:10
# 补充
原生 JDBC 的 java.sql.Statement 中有 getGeneratedKeys() 方法可以获取。
4.2 delete
<delete id="deleteEmp" parameterType="java.lang.Integer">
delete from tbl_employee where id = #{id}
</delete>
4.3 update
<update id="updateEmp" parameterType="com.hedon.mybatis.bean.Employee">
update tbl_employee set last_name = #{lastName}, gender=#{gender},email=#{email} where id = #{id}
</update>
△ 补充:insert、update、delete 标签的相关属性:
4.4 select
4.4.1 返回单个对象
- resultType:即为返回对象的类型。
Employee getEmployeeById(Integer id);
<select id="getEmployeeById" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee where id = #{id}
</select>
4.4.2 返回集合
- resultType:是 List 的 T 的类型,MyBatis 会自动为我们将对象封装到集合中。
List<Employee> getEmps();
<select id="getEmps" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee
</select>
4.4.3 返回一条记录的 Map
- resultType:map。
- MyBatis 会为我们自动封装,key 就是列名,value 就是属性的值。
Map<String,Object> getEmployeeMapById(Integer id);
<select id="getEmployeeMapById" resultType="map">
select * from tbl_employee where id = #{id}
</select>
结果:
{gender=1, last_name=hedon, id=1, email=171725713@qq.com}
4.4.4 返回多条记录的 Map
- resultType:这个时候 resultType 还是写 Employee 的全限定类名。
这个时候问题就来了,Map 中的 value 已经解决了,那 key 是什么呢?这里就需要用到 @MapKey
来指定用 Employee 中的哪一个属性来作为 key。
@MapKey("id")
Map<String,Employee> getEmployeesMap();
<select id="getEmployeesMap" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee
</select>
结果:
{1=Employee{id=1, lastName='hedon', gender=1, email='171725713@qq.com'},
9=Employee{id=9, lastName='AAAAABC', gender=?, email='!234'},
10=Employee{id=10, lastName='AAAAABC', gender=?, email='!234'}}
4.5 参数处理
4.5.1 单个参数
单个参数的时候 MyBatis 不会做特殊处理,直接 #{随意值}
就可以取出参数的值。
4.5.2 多个参数
4.5.2.1 错误示例
<select id="getEmpByIdAndName" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee where id = #{id} and last_name = #{lastName}
</select>
- 报错信息:
Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
-
原因分析:
面对多个参数的时候,MyBatis 会做特殊处理。它会将多个参数封装成一个 map。这个 map 有以下规则:
- key:param1,param2,……,paramN 或者 arg0,arg1,arg2,……argn
- value:参数值
4.5.2.2 正确样例
<select id="getEmpByIdAndName" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee where id = #{arg0} and last_name = #{arg1}
</select>
4.5.3 命名参数
所谓命名参数也就是自己来指定上述 map 的 key,而不用 MyBatis 的默认规则。
首先要在 Mapper 接口类方法传参中加入 @Param 注解:
public interface EmployeeMapper {
Employee getEmpByIdAndName(@Param("id") Integer id, @Param("lastName") String lastName);
}
这样就可以直接指定 key 了:
<select id="getEmpByIdAndName" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee where id = #{id} and last_name = #{lastName}
</select>
4.5.4 POJO
如果是传对象的话,可以直接用 #{属性名}
来取出对象中对应的属性值。
public class Employee {
private Integer id;
private String lastName;
private String gender;
private String email;
}
<update id="updateEmp" parameterType="com.hedon.mybatis.bean.Employee">
update tbl_employee set last_name = #{lastName}, gender=#{gender},email=#{email} where id = #{id}
</update>
一个思考题:下面的传参该如何取值(根据 id 和 lastName 来查询)?
Employee getEmp(Integer id, Employee employee);
因为都没有加 @Param,所以会被 MyBatis 自动封装到 Map 中,且 key 为 param1,param2,……,paramn。但是因为 Employee 是一个对象,所以我们要取出 employee 里面的 lastName 的话,只需要用 “.” 就可以了。综上,如下:
select * from tbl_employee where id = #{param1} and last_name = #{param2.lastName}
4.5.5 Map
如果没有现成的 POJO,那么我们可以传 map,然后用 #{key}
取出 map 中的值。
<select id="getEmpByMap" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee where id = #{id} and last_name = #{lastName}
</select>
测试:
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("id",1);
map.put("lastName","hedon");
Employee empByMap = mapper.getEmpByMap(map);
System.out.println(empByMap); //Employee{id=1, lastName='hedon', gender=1, email='171725713@qq.com'}
4.5.6 DTO
每次都要封装一个 map 的话就太麻烦了,我们可以直接封装成一个类,这个类我们一般称为 DTO(Data Transfer Object),是专门用来做数据传输的,原理如上,不赘述。
4.5.7 集合类
集合类的话 MyBatis 也会进行特殊处理,也是把传入的 list 或 array 存入 map 中,但是这个时候 key 就有特殊规定了:
参数 | key |
---|---|
Connection | connection |
List | list |
Array | array |
如下:
# 接口方法
Employee getEmployeesByIds(List<\Integer> ids)
# Mapper 映射文件
我们现在要取出 ids 中的第一个值
select * from tbl_employee where id = #{list[0]}
4.5.8 传参封装 Map 的源码解读
待补。
4.5.9 # 和 $ 的区别
最大的区别就在于 填充
的方式不同。我们可以直接举个例子来看看它输出的 SQL 语句的不同:
<select id="getEmpByMap" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee where id = ${id} and last_name = #{lastName}
</select>
以上我们采用 $ 来取出 id 值,用 # 来取出 lastName 的值。运行 getEmpByMap() 方法,查看 SQL 语句:
我们可以清楚的看到:
- $ 取出来的值会直接放到 SQL 语句中,也就是传统的拼接方式。
- # 取出来的值不会直接放到 SQL 语句中,SQL 语句会在要填充值的地方用 ? 来占位,然后再把 # 取出来的值赋值进去。
# SQL 注入***
$ 取出来的值直接放到 SQL 语句中,也就是下面这种方式
method(String id){
String sql = "select * from tbl_employee where id = '" + id + "'";
}
如果 id = “' or 1=1 or id='”
那么拼接起来就是 select * from tbl_employee where id = '' or 1=1 or id=''
里面有一个 1=1,又用的是 or,所以会取出所有的 employee 的信息,这样就会造成数据泄露。
# 什么时候用 $ ?
在`分表查询`的时候,如下按照年份进行分表,那么我们就需要这么查询:
select * from ${year}_salary where xxx;
数据库表名是不能用 #{} 来取的,又因为原生的 JDBC 也不支持占位符的方式,所以我们都只能用拼接的方式来进行分表查询。
4.5.10 # 取值时指定参数相关规则
在用 # 进行取值的时候,还可以规定参数的一些规则,如:
- javaType
- jdbcType
- mode:存储过程
- numericScale
- resultMap
- typeHandler
- jdbcTypeName
- expression(未来准备支持的功能)
# 重点记录一下 jdbcType
在我们数据为 null 的时候,有些数据库是不能识别 MyBatis 对 null 的默认处理的。比如 Oracle,它会报错。
因为 MyBatis 默认会将 null 的数据设置为 JdbcType.OTHER 类型的,这个类型 Orcle 不能识别,而 MySQL 可以识别。
这里有两种解决途径:
① 主动指定 null 类型的处理,如:#{email,jdbcType=NULL}
② 在 MyBatis 全局配置文件中设置:<setting name="jdbcTypeForNull" value="NULL"></setting>
4.6 属性映射
4.6.1 自动映射
4.6.1.1 autoMappingBehavior
在 MyBatis 的全局配置的 setting
中有一个属性 autoMappingBehavior
,它的值是 PARTIAL
,默认开启自动映射的功能。唯一的要求是列名和 javaBean 属性名一致。
如果我们将 autoMappingBehavior
设置为 null
,就会关闭自动映射。
4.6.1.2 mapUnderscoreToCamelCase
在前面的基础上,如果我们的 POJO 属性命名符合驼峰命名法
,那我们也可以开启自动驼峰命名规则映射功能,即将 mapUnderscoreToCamelCase
设置为 true
。
4.6.2 自定义 ResultMap,实现自定义结果集映射
4.6.2.1 单个类,普通属性
- resultMap:自定义某个 javaBean 的封装规则
- type:自定义规则的 Java 类型
- id:resultMap 唯一的 id 索引
- column:指定哪一列(数据库中的)
- property:指定哪一个属性(javaBean 中的)
<resultMap id="map" type="com.hedon.mybatis.bean.Employee">
<!--指定 id 的话 MyBatis 底层会有优化处理-->
<id column="id" property="id"/>
<!--定义普通列封装规则-->
<result column="last_name" property="lastName"/>
<!--如果其他的列不写,则按默认规则封装,建议写全-->
<result column="email" property="email"/>
<result column="gender" property="gender"/>
</resultMap>
<select id="getEmpById" resultMap="map">
select * from tbl_employee where id = #{id}
</select>
4.6.2.2 类中的属性是个对象 —— 级联查询
法一:用 association 表示级联查询
- property:Employee 类中的属性值
- javaType:property 是哪种类型的(不可省略)
<resultMap id="mapWithDept" type="com.hedon.mybatis.bean.Employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<association property="department" javaType="com.hedon.mybatis.bean.Department">
<id column="id" property="id"/>
<result column="dept_name" property="departmentName"/>
</association>
</resultMap>
<select id="getEmpWithDept" resultMap="mapWithDept">
select e.id,e.last_name,e.email,e.gender,e.d_id,d.dept_name from tbl_employee e, tbl_dept d where e.d_id = d.id and e.id = #{id}
</select>
法二:用 “.” 表示级联查询
<resultMap id="mapWithDept" type="com.hedon.mybatis.bean.Employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<result column="d_id" property="department.id"/>
<result column="dept_name" property="department.departmentName"/>
</resultMap>
<select id="getEmpWithDept" resultMap="mapWithDept">
select e.id,e.last_name,e.email,e.gender,e.d_id,d.dept_name from tbl_employee e, tbl_dept d where e.d_id = d.id and e.id = #{id}
</select>
4.6.2.3 分步查询
我们还可以先查询 Employee,得到 dId,再用 dId 去查询 Department 的信息:
<resultMap id="mapByStep" type="com.hedon.mybatis.bean.Employee">
<!--第一步:按照员工Id查询出员工信息-->
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<!--第二步:按照部门ID取查询部门信息
select:表明当前这个属性是根据哪个方法来查询得到的
column:传给另外一个方法的参数
-->
<association property="department"
select="com.hedon.mybatis.dao.DepartmentMapper.getDeptById"
column="d_id">
</association>
</resultMap>
<select id="getEmployeeWithDeptByStep" resultMap="mapByStep">
select * from tbl_employee where id = #{id}
</select>
4.2.6.4 延迟加载
我们每次在查询 Employee 的时候,都把 Department 也一次性查询出来。其实我们可以在需要部门信息的时候再去查询,也就是所谓的延迟加载。
为了实现上述目的,我们需要在分步查询的基础上加上 2 个配置。来到 MyBatis 全局配置文件上:
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启懒加载支持,默认为 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- true:属性一次性加载; false:按需加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
测试:
当我们要拿到部门信息的时候,才会继续去查询部门信息:
4.2.6.5 一对多
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Department {
private Integer id;
private String departmentName;
private List<Employee> employees;
}
如上,一个部门对应多个员工,我们现在希望在查询部门的时候将属于这个部门的员工也一并查询出来。
Department getDeptWithEmpsById(Integer id);
<resultMap id="deptWithEmpsMap" type="com.hedon.mybatis.bean.Department">
<id column="id" property="id"/>
<result column="dept_name" property="departmentName"/>
<!-- 集合的话就用 collection, ofType 指定集合里面的类型 -->
<collection property="employees" ofType="com.hedon.mybatis.bean.Employee">
<!--在 collection 中定义集合中元素的属性的封装规则-->
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
</collection>
</resultMap>
<select id="getDeptWithEmpsById" resultMap="deptWithEmpsMap">
select d.id, d.dept_name, e.id, e.last_name, e.gender, e.email
from tbl_dept d
left join tbl_employee e
on d.id = e.d_id
where d.id = #{id}
</select>
4.2.6.6 一对多 —— 分步查询
第一步:查询部门信息
第二步:根据部门ID去查询员工信息
Department getDeptWithEmpsByIdByStep(Integer id);
<resultMap id="byStepMap" type="com.hedon.mybatis.bean.Department">
<id column="id" property="id"/>
<result column="dept_name" property="departmentName"/>
<!--第2步:根据部门ID查询员工信息-->
<collection property="employees"
select="com.hedon.mybatis.dao.EmployeeMapperPlus.getEmpsByDeptId"
column="id"/>
</resultMap>
<!--第1步:查询部分信息-->
<select id="getDeptWithEmpsByIdByStep" resultMap="byStepMap">
select d.id,d.dept_name from tbl_dept d where id = #{id}
</select>
这里需要在 EmployeeMapperPlus 中加一个 getEmpsByDeptId 方法:
List<Employee> getEmpsByDeptId(Integer id);
<select id="getEmpsByDeptId" resultType="com.hedon.mybatis.bean.Employee">
select * from tbl_employee where d_id = #{id}
</select>
这样就完成一对多的分步查询了。
4.2.6.7 一对多 —— 延迟加载
在前面在 MyBatis 全局配置文件 mybatis-config.xml
中设置了懒加载的基础上,一对多也会自动懒加载:
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启懒加载支持 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- true:属性一次性加载; false:按需加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
4.2.6.8 分步查询 —— 传递多列
如果在分步查询的时候需要传多个值,那就需要将多个值封装到 map
里面来传递,如:
coloum="{key1=column1,key2=column2}"
4.7 鉴别器
MyBatis 可以使用 discriminator
鉴别器来判断某列的值,然后根据某列的值改变封装行为。
Employee getEmpWithDiscriminator(Integer id);
<!--
实现功能:
① 如果查出的是女生(gender=0),就把部门信息查出来,否则不查询;
② 如果查出的是男生(gender=1),就把 last_name 这一列的值赋值给 email
-->
<resultMap id="MyEmpWithDiscriminator" type="com.hedon.mybatis.bean.Employee">
<id column="id" property="id"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<result column="last_name" property="lastName"/>
<!--鉴别器
column: 指定判定的列名
javaType: 列值对应的类型
-->
<discriminator javaType="String" column="gender">
<!--女生-->
<case value="0" resultType="com.hedon.mybatis.bean.Employee">
<!--如果是女生,就查询部门信息-->
<association property="department"
select="com.hedon.mybatis.dao.DepartmentMapper.getDeptById"
column="d_id">
</association>
</case>
<!--男生-->
<case value="1" resultType="com.hedon.mybatis.bean.Employee">
<id column="id" property="id"/>
<result column="gender" property="gender"/>
<!--不查部门信息,并把 last_name 的值赋值给 email-->
<result column="last_name" property="email"/>
<result column="last_name" property="lastName"/>
</case>
</discriminator>
</resultMap>
<select id="getEmpWithDiscriminator" resultMap="MyEmpWithDiscriminator">
select * from tbl_employee where id = #{id}
</select>
查询一个男生:
@Test
public void test5() throws IOException{
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee emp = mapper.getEmpWithDiscriminator(1);
System.out.println(emp);
}finally {
sqlSession.close();
}
in.close();
}
查询一个女生:
Employee emp = mapper.getEmpWithDiscriminator(10);
5. 动态 SQL
5.0 OGNL 表达式
访问对象属性: person.name
调用方法:person.getName()
调用静态属性:@java.lang.Math@PI
调用静态方法:@java.util.UUID@randomUUID()
调用构造方法:new com.heon.bean.Employee(“name”).name
算术运算符:+ - * / %
逻辑运算符:in,not in,>,>=,>,>=,==,!=
访问集合伪属性:
类型 | 伪属性 | 伪属性对应的 java 方法 |
---|---|---|
List、Set、Map | size、iSEmpty | List/Set/Map.size(),List/Set/Map.isEmpty() |
List、Set | iterator | List.iterator()、Set.iterator() |
Map | keys、values | Map.keySet()、Map.values() |
Iterator | next、hasNext | Iterator.next()、Iterator.hasNext() |
5.1 if
List<Employee> getEmpsByCondition(Employee employee) ;
<!--查询员工:携带哪个字段就按这个字段来查询-->
<select id="getEmpsByCondition" resultType="com.hedon.bean.Employee">
SELECT * FROM tbl_employee
WHERE 1=1 <!-- 1=1 是为了连接后面,不然如果只剩一个 where 的话语法是错误的 -->
<!--text: 判断表达式(OGNL)-->
<if test="id!=null">
AND id = #{id}
</if>
<if test="lastName!=null and lastName.trim()!=''">
AND last_name like #{lastName}
</if>
<if test="email!=null and email.trim()!=''">
AND email = #{email}
</if>
<if test="gender==0 or gender==1">
AND gender = #{gender}
</if>
</select>
测试:带了 id 和 email
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = new Employee();
employee.setId(1);
employee.setEmail("1111");
List<Employee> empsByCondition = mapper.getEmpsByCondition(employee);
System.out.println(empsByCondition);
测试:只带 lastName
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = new Employee();
employee.setLastName("hedon");
List<Employee> empsByCondition = mapper.getEmpsByCondition(employee);
System.out.println(empsByCondition);
由上可以看到我们实现了 携带哪个字段就按这个字段来查询
的功能。
5.2 where
上面我们在拼接条件的时候,为了防止 where
后面没有字段而导致 SQL 语法错误我们就加上了 where 1=1
。这是可以解决问题的,但是似乎不太优雅。MyBatis 提供了一个新的标签 where
可以帮助我们解决这个问题。
<!--查询员工:携带哪个字段就按这个字段来查询-->
<select id="getEmpsByCondition" resultType="com.hedon.bean.Employee">
SELECT * FROM tbl_employee
<!--where 会自动帮我们把前面多余的 and 和 or 去掉-->
<where>
<if test="id!=null">
AND id = #{id}
</if>
<if test="lastName!=null and lastName.trim()!=''">
AND last_name like #{lastName}
</if>
<if test="email!=null and email.trim()!=''">
AND email = #{email}
</if>
<if test="gender==0 or gender==1">
AND gender = #{gender}
</if>
</where>
</select>
5.3 trim
上面的 where
有一个问题,它只能去掉 前面的 AND
。如果 AND 是写在后面的话,如:
<!--查询员工:携带哪个字段就按这个字段来查询-->
<select id="getEmpsByCondition" resultType="com.hedon.bean.Employee">
SELECT * FROM tbl_employee
<!--where 会自动帮我们把前面多余的 and 和 or 去掉-->
<where>
<if test="id!=null">
id = #{id} AND
</if>
<if test="lastName!=null and lastName.trim()!=''">
last_name like #{lastName} AND
</if>
<if test="email!=null and email.trim()!=''">
email = #{email} AND
</if>
<if test="gender==0 or gender==1">
gender = #{gender}
</if>
</where>
</select>
那么当 gender
属性为 null 的话,SQL 语句最后就会多出一个 AND 而报错。
如果要解决这个问题,一个思路就是老老实实把 AND 写在前面,另外一个思路就是可以使用 trim
标签,它有 4 个属性:
- prefix:前缀。给 trim 标签拼串后的整个字符串加一个前缀。
- prefixOverrides:前缀覆盖。去掉整个字符串前面多余的字符,如 AND,OR
- suffix:后缀。给 trim 标签拼串后的整个字符串加一个后置。
- suffixOverrides:后缀覆盖。去掉整个字符串前面多余的字符,如 AND,OR
这样不管是在前面加 AND 还是在后面加 AND,我们都有办法应对了。
<!--查询员工:携带哪个字段就按这个字段来查询-->
<select id="getEmpsByCondition" resultType="com.hedon.bean.Employee">
SELECT * FROM tbl_employee
<trim prefix="where" prefixOverrides="AND" suffixOverrides="AND">
<if test="id!=null">
id = #{id} AND
</if>
<if test="lastName!=null and lastName.trim()!=''">
last_name like #{lastName} AND
</if>
<if test="email!=null and email.trim()!=''">
email = #{email} AND
</if>
<if test="gender==0 or gender==1">
gender = #{gender}
</if>
</trim>
</select>
5.4 choose(when/otherwise)
前面的三个标签都是把所有的条件都拼接起来,如果你只带了 id,就那按 id 查询,如果你 id 和 lastName 一起带了,那就按这两个条件来查询。下面的这个 choose
标签,其实是类似于我们的 switch-case
语句,只选择一个属性进行查询。
<select id="getEmpsByCondition" resultType="com.hedon.bean.Employee">
SELECT * FROM tbl_employee
<where>
<choose>
<when test="id!=null">
id = #{id}
</when>
<when test="lastName!=null and lastName.trim()!=''">
last_name = #{lastName}
</when>
<when test="email!=null and email.trim()!=''">
email = #{email}
</when>
<otherwise>
gender = 1
</otherwise>
</choose>
</where>
</select>
测试:只带 lastName
测试:同时带 id 和 lastName
测试:都不带
5.5 set
前面都是在封装查询条件,而 set
标签是用来封装我们的修改条件的。
void updateEmp(Employee employee);
<update id="updateEmp" parameterType="com.hedon.bean.Employee">
UPDATE tbl_employee
<set>
<if test="lastName!=null and lastName.trim()!=''">
last_name = #{lastName},
</if>
<if test="email!=null and email.trim()!=''">
email = #{email},
</if>
<if test="gender==0 or gender ==1">
gender = #{gender}
</if>
</set>
WHERE id = #{id}
</update>
测试:
employee.setId(1);
employee.setLastName("lalalal");
employeeMapper.updateEmp(employee);
也可以使用 trim
来代替,也可以配置自动去掉拼接后多出的逗号:
<update id="updateEmp" parameterType="com.hedon.bean.Employee">
UPDATE tbl_employee
<trim prefix="set" suffixOverrides=",">
<if test="lastName!=null and lastName.trim()!=''">
last_name = #{lastName},
</if>
<if test="email!=null and email.trim()!=''">
email = #{email},
</if>
<if test="gender==0 or gender ==1">
gender = #{gender}
</if>
</trim>
WHERE id = #{id}
</update>
5.6 foreach
我们可以用 foreach
标签来遍历传进来的集合参数:
- collection:指定要遍历的集合,可以有 3 种值,collection,list,map
list 类型的参数会特殊处理封装在 map 中,map 的 key 就叫 list
-
item:指当前遍历出的元素赋值给特定的变量
-
separator:每个元素之间的分隔符
-
open:遍历出所有结果后拼接一个开始的字符,如果集合为空的话是不会拼的
-
close:遍历出所有结果后拼接一个结束的字符,如果集合为空的话是不会拼的
-
index:索引。
- 遍历 list 的时候 index 就是索引,item 就是当前值
- 遍历 map 的时候 index 表示的是 map 的key,item 表示的是值
-
#{变量名}:取出变量的值
5.6.1 批量查询
<select id="getEmpsByIds" resultType="com.hedon.bean.Employee">
SELECT * FROM tbl_employee
<foreach collection="list" item="item" separator="," open="WHERE id IN (" close=")" index="index">
#{item}
</foreach>
</select>
5.6.2 批量插入一
<insert id="insertEmps">
INSERT INTO tbl_employee(last_name,email,gender,d_id)
VALUES
<foreach collection="list" item="emp" separator=",">
(#{emp.lastName},#{emp.email},#{emp.gender},#{emp.department.id})
</foreach>
</insert>
上述可以实现批量插入 Employee,但是如果传进来的 list 是空的话,就会报错。
5.6.3 批量插入二
<insert id="insertEmps">
<foreach collection="list" item="emp" separator=";">
INSERT INTO tbl_employee(last_name,email,gender,d_id)
VALUES (#{emp.lastName},#{emp.email},#{emp.gender},#{emp.department.id})
</foreach>
</insert>
这种方式需要开启 MySQL 的多语句执行:allowMultiQueries=true
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
5.7 bind
bind 可以将一个 OGNL 表达式的值绑定到一个变量中,方便后面使用。
List<Employee> getEmpsByLastName(String lastName);
<select id="getEmpsByLastName" resultType="com.hedon.bean.Employee">
<bind name="_lastName" value="'%'+lastName+'%'"/>
SELECT * FROM tbl_employee WHERE last_name LIKE #{_lastName}
</select>
List<Employee> emps = mapper.getEmpsByLastName("h");
比如上面,我们希望进行模糊查询,并且不希望在传参的时候加上 %%,但是如果我们想用
<select id="getEmpsByLastName" resultType="com.hedon.bean.Employee">
SELECT * FROM tbl_employee WHERE last_name LIKE '%#{_lastName}%'
</select>
来完成目的是不成功的,#{} 是不支持拼接的。
虽然我们可以用 ${} 来实现:
<select id="getEmpsByLastName" resultType="com.hedon.bean.Employee">
SELECT * FROM tbl_employee WHERE last_name LIKE '%${_lastName}%'
</select>
但是这样就可能会有 SQL 注入***的风险。
所以我们就可以用 bind
来进行绑定,用 _lastName
来替代 %lastName%
,从而实现模糊查询。
5.8 sql
sql 标签用来抽取可重用的字段,方便后面引用,也支持动态判断。
<sql id="basicColumn">
id,name,gender,address
</sql>
<seletc id="getUsersInfo">
select
<include refid="basicColumn"/>
from tbl_employee
</seletc>
6. 两个内置参数
MyBatis 会内置两个参数,任何时候都可以取出来进行一下判断和操作。
_parameter:代表整个参数。
- 单个参数:_parameter 就是这个参数
- 多个参数:参数会被封装为一个 map,_parameter 就是代表这个 map
_databaseId:如果配置了 databaseIdProvicder 标签,_dataBaseId 就是代表当前数据库的别名。
7. 缓存
MyBatis 系统中默认定义了两级缓存。
7.1 一级缓存
7.1.1 概念
与数据库痛一次会话期间(同一个 SqlSession)查询到的数据会放在本地缓存汇总中。以后如果需要获取相同的数据,直接从缓存中拿,没必 要再去查询数据库。
7.1.2 演示
@Test
public void test() throws IOException {
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
System.out.println("第一次:");
Employee emp = employeeMapper.getEmpById(1);
System.out.println(emp.hashCode());
System.out.println("第二次:");
Employee emp2 = employeeMapper.getEmpById(1);
System.out.println(emp2.hashCode());
sqlSession.close();
in.close();
}
7.1.3 失效情况
- SqlSession 变了
- 查询条件不同
- 两次查询之间执行过增删改操作
- 手动清除一级缓存
clearCache()
7.1.4 作用域
在 mybatis3.1 之后,可以配置本地缓存的作用域. 在 mybatis.xml 中配置:
7.2 二级缓存
7.2.1 概念
二级缓存是基于 namespace
级别的缓存,一个 namespace
对应一个二级缓存。
默认不开启,需要手动开启。
MyBatis 提供二级缓存的接口以及实现,缓存实现要求 POJO 实现 Serializable
接口。
二级缓存在 SqlSession
关闭或提交之后才会生效。
7.2.2 工作机制
- 一个会话,查询一条数据,这个数据就会被放在当前会话的以及缓存中
- 如果会话关闭,一级缓存中的数据就会被保存到二级缓存中,新的会话查询信息,就可以参照二级缓存
- 如果在一个 sqlSession 中即用了 EmployeeMapper 查询 Employee 信息,又用了 DepartmentMapper 查询了 Department 信息,那么,这是属于两个不同的 namespace 的。不同 namespace 查出的数据会放在自己对应的缓存中(map)。
7.2.3 使用
-
开启全局二级缓存
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 开启二级缓存 --> <setting name="cacheEnabled" value="true"/> </settings>
-
去 Mapper.xml 配置二级缓存
<cache/>
-
可以再设置相关属性
-
eviction:缓存回收策略
- LRU – 最近最少使用的:移除最长时间不被使用的对象。(默认)
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
-
flushInterval:刷新间隔,单位毫秒
- 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
-
size:引用数目,正整数
- 代表缓存最多可以存储多少个对象,太大容易导致内存溢出。
-
readOnly:只读
-
true:只读缓存。会给所有调用者返回缓存对象的相同实例。因为这些对象不能被修改。这提供了很重要的性能优势。
-
false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些, 但是安全,因此默认是 false。
-
-
-
缓存的相关设置
-
全局 setting 的
cacheEnable
:配置二级缓存的开关。一级缓存一直是打开的。 -
select 标签的
useCache
属性:配置这个 select 是否使用二级缓存。一级缓存一直是使用的。 -
sql 标签的
flushCache
属性:增删改默认flushCache=true。sql执行以后,会同时清空一级和二级缓存。查询默认flushCache=false。 -
sqlSession.clearCache():只能用来清空一级缓存。
-
当在某一个作用域 (一级缓存Session/二级缓存 Namespaces) 进行了 C/U/D 操作后,默认该作用域下所 有 select 中的缓存将被clear。
-
7.2.4 演示
先让 Employee 继承 Serializable 接口:
public class Employee implements Serializable {
在全局配置文件 mybatis-config.xml 中开启二级缓存
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
在 SQL 映射文件中开始二级缓存支持
<cache eviction="LRU" readOnly="false"></cache>
测试
@Test
public void test2() throws IOException{
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
EmployeeMapper employeeMapper1 = sqlSession1.getMapper(EmployeeMapper.class);
EmployeeMapper employeeMapper2 = sqlSession2.getMapper(EmployeeMapper.class);
Employee emp1 = employeeMapper1.getEmpById(1);
System.out.println(emp1);
sqlSession1.close();//必须关闭第一个sqlSession,数据才会从一级缓存转移到二级缓存
Employee emp2 = employeeMapper2.getEmpById(1);
System.out.println(emp2);
sqlSession2.close();
in.close();
}
8. 整合 Sping
9. 逆向工程