ORM思想之源码分析篇:MyBatis的ORM思想分析
1. ORM的经典应用:MyBatis案例
MyBatis 框架也应用了 ORM 思想,一般我们把它称之为半自动的 ORM 框架,跟 Hibernate相比,MyBatis 更加轻量,更加灵活,为了保证这一点,程序员在使用 MyBatis 时需要自己编写 sql 语句,但是 API 的使用依然像 Hibernate 一样简单方便.
1.1 创建 Maven 工程,数据库
1.2 编写 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>top.lzchao.orm</groupId>
<artifactId>MyBatisDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
</project>
1.3 MyBatis 核心配置文件 SqlMapConfig.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>
<typeAliases> <!-- 配置别名 -->
<package name="top.lzchao.orm.entity"/>
</typeAliases>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test" />
<property name="username" value="root" />
<property name="password" value="123456" />
</dataSource>
</environment>
</environments>
<mappers> <!-- 加载 mapper 文件 -->
<package name="top.lzchao.orm.mapper"/>
</mappers>
</configuration>
该配置文件主要设置了数据库连接信息和映射配置文件的位置信息
1.4 实体类 DeptEmp.java
/**
* @author: LzCc
* @blog: https://blog.csdn.net/qq_41744145
* @description: 存储各部门的员工总数的实体类
*/
public class DeptEmp {
private String deptName; //部门名称
private int total; //员工总数
// getter、setter...
// toString...
}
这就是一个普通的实体类,用来封装和存储查询出来的数据
1.5 Mapper 接口 DeptEmpMapper.java
/**
* @author: LzCc
* @blog: https://blog.csdn.net/qq_41744145
* @description: Mapper 接口
*/
public interface DeptEmpMapper {
//查询各部门的员工总数
List<DeptEmp> getEmpTotalByDept();
}
这是 Mapper 接口,我们只需要定义方法,由 MyBatis 框架创建代理类(实现类)并实现功能
1.6 映射配置文件 DeptEmpMapper.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="top.lzchao.orm.mapper.DeptEmpMapper">
<!--自定义 resultMap 配置实体类和结果集之间的映射关系-->
<resultMap type="DeptEmp" id="dept_emp_result_map">
<result property="deptName" column="dname"/>
<result property="total" column="total"/>
</resultMap>
<!--定义查询语句-->
<select id="getEmpTotalByDept" resultMap="dept_emp_result_map">
select
dname, count(*) as total
from
dept,emp
where
emp.dept_id=dept.did
group by
dname
</select>
</mapper>
MyBatis 其实会采用默认规则自动建立表和实体类之间,属性和字段之间的映射关系,但是由于我们这个案例中是要查询各部门的员工总数,无法自动映射,所以我们自定义了一个resultMap 来配置实体类和查询结果之间的映射关系,想通过这个案例更加明显的体现一下ORM 思想。
1.7 测试类
/**
* @author: LzCc
* @blog: https://blog.csdn.net/qq_41744145
* @description: 测试类
*/
public class DeptEmpTest {
private SqlSessionFactory sqlSessionFactory = null;
@Before
public void init() throws Exception {
// 创建 SQLSessionFactoryBuilder 对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 加载配置文件
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
// 创建 SQLSessionFactory 对象
sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
}
@Test
public void testGetEmpTotalByDept() {
//得到 sqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//得到 Mapper 代理对象
DeptEmpMapper deptEmpMapper = sqlSession.getMapper(DeptEmpMapper.class);
//调用方法实现查询各部门员工总数
List<DeptEmp> list = deptEmpMapper.getEmpTotalByDept();
for (DeptEmp de : list) {
System.out.println(de.getDeptName() + " " + de.getTotal());
}
//关闭 sqlSession
sqlSession.close();
}
}
该测试类通过 MyBatis 的 API 实现了查询功能,如下图所示:
2. MyBatis 的 ORM 实现原理
接下来我们通过上述案例来讲解一下 MyBatis 框架是如何应用 ORM 思想的,一起剖析一下 MyBatis 3.x 的内部实现原理。
2.1 解析 MyBatis 核心配置文件并创建 SqlSessionFactory 对象
// 加载配置文件
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
// 创建 SQLSessionFactoryBuilder 对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 创建 SQLSessionFactory 对象
sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
这三行代码先是创建了 SqlSessionFactoryBuilder 对象,然后获得了 MyBatis 核心配置文件的输入流,最后调用了 build 方法,我们接下来跟踪一下该方法的源码:
第 77 行代码创建了一个 xml 解析器对象,并在第 78 行对 MyBatis 核心配置文件进行了解析,拿到了数据库连接数据以及映射配置文件中的数据(包括我们编写的 sql 语句和自定义的resultMap),如下图所示:
第 92 行代码创建了 DefaultSqlSessionFactory 对象,并把上图中展示的解析后拿到的数据传给了它,我们继续跟踪
在 DefaultSqlSessionFactory 的构造方法中,我们发现,解析后拿到的数据最终被全部存入到了一个名字为 Configuration 的对象中,后续很多操作都会从该对象中获取数据。
2.2 SqlSession sqlSession = sqlSessionFactory.openSession();
图中红框部分代码,主要通过读取 Configuration 对象中的数据分别创建了 Environment 对象,事务对象,Executor 对象等,并最终直接 new 了一个 DefaultSalSession 对象(SqlSession 接口的实现类),该对象是 MyBatis API 的核心对象
2.3 DeptEmpMapper deptEmpMapper = sqlSession.getMapper(DeptEmpMapper.class);
第 292 行代码调用了 Configuration 对象的 getMapper 方法,把实体类和 sqlSession 对象传了过去
第 710 行代码调用了 MapperRegistry 对象的 getMapper 方法,把实体类和 sqlSession 对象传
了过去
第 50 行代码通过代理工厂最终把我们自定义的 Mapper 接口的代理对象创建了出来
2.4 List list = deptEmpMapper.getEmpTotalByDept();
当我们调用代理对象的 getEmpTotalByDept()方法时,框架内部会调用 MapperProxy 的 invoke 方法, 我们可以观察一下这个方法三个参数的值,如下图所示:
在 invoke 方法内锁定第 53 行代码,该行代码调用 MapperMethod 的 execute 方法实现查询功能
在 execute 方法内先进行增删改查判断,本案例进行的是查询,并且可能会查询出多条记录,所以最后锁定第 60 行代码,该行代码调用 executeForMany 方法进行查询
第 124 行代码通过 hasRowBounds()方法判断是否进行分页查询,本案例不需要,所以执行 else 中的代码,调用 sqlSession 的 selectList 方法(第 128 行), 参数的值如下图所示
第 147 行代码从 Configuration 对象中获得 MappedStatement 对象,该对象存储了映射配置文件中的所有信息(包括我们编写的 sql 语句和自定义的 resultMap),如下图所示
第 148 行代码调用了 CachingExecutor 的 query(…)方法, 继续往下跟踪
第 81 行通过 ms 对象从 mapper 映射配置文件中获得了 sql 语句,如下图所示:
第 83 行代码调用了下面的 query 方法,该方法内部先从缓存中取数据,如果缓存中没数据,
就重新查询(第 89 行),继续往下跟踪
第 156 行代码通过调用 queryFromDatabase 方法最终执行了 sql 语句并将查询结果按照我们自定义的 resultMap 进行了封装,结果如下图所示、