ORM 思想及相关框架(Hibernate以及MyBatis)实现原理
该课程要求程序员必须已经基本掌握 xml 解析,反射,JDBC,Hibernate,MyBatis,Maven 等相关技术
课程大纲
ORM 思想
ORM 的经典应用:Hibernate 案例及实现原理
ORM 的经典应用:MyBatis 案例及实现原理
自定义一个 ORM 框架:MiniORM
一.ORM 思想
目前,通过 Java 语言连线并操作数据库的技术或方式已经有很多了,例如:JDBC, Hibernate,MyBatis,TopLink 等等。其中 JDBC 是 Java 原生的 API,支援连线并操作各种关系型数据库。相信每个程序员都是从 JDBC 开始学起的,然后才接触到各种持久层框架。JDBC 作为 Java 原生 API,有优点,也有缺点,这里主要说一下缺点:
1.编码繁琐,效率低
2.数据库连线的建立和释放比较重复,也造成了系统资源的浪费
3.大量硬编码,缺乏灵活性,不利于后期维护
4.引数的赋值和资料的封装全是手动进行
… …
可能有些程序员还可以再列出一些 JDBC 的缺点,如果你已经很久没有使用过 JDBC 了, 印象已经不深刻了,那么相信下面的程式码能勾引起你的些许回忆。
ublic List findAll() { Connection connection = null;
PreparedStatement preparedStatement = null; ResultSet resultSet = null;
List bookList = null;
try {
//载入数据库驱动Class.forName("com.mysql.jdbc.Driver");
//通过驱动管理类获取数据库连结
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");
//定义 sql 语句 ?表示占位符
String sql = "select * from t_book where author = ?";
//获取预处理 statement
preparedStatement = connection.prepareStatement(sql);
//设定引数,第一个引数为 sql 语句中引数的序号(从 1 开始),第二个引数为设定的引数值
preparedStatement.setString(1, "张三");
//向数据库发出 sql 执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
//遍历查询结果集
bookList = new ArrayList(); while(resultSet.next()){
Book book=new Book(); book.setId(resultSet.getInt("id")); book.setName(resultSet.getString("bname")); book.setAuthor(resultSet.getString("author")); book.setPrice(resultSet.getDouble("price")); bookList.add(book);
}
return bookList;
} catch (Exception e) { e.printStackTrace(); return null;
}finally{
//释放资源if(resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) { e.printStackTrace();
}
}
if(preparedStatement!=null){ try {
preparedStatement.close();
} catch (SQLException e) { e.printStackTrace();
}
}
if(connection!=null){ try {
connection.close();
} catch (SQLException e) { e.printStackTrace();
}
}
}
}
正是因为 JDBC 存在着各种问题,所以才导致很多持久层框架应运而生,例如:Hibernate 和 MyBatis,这两个都是目前比较流行的持久层框架,都对 JDBC 进行了更高阶的封装和优化,相信大家对这两个框架都比较熟悉。
很多程序员其实都亲自尝试过自己对 JDBC 进行封装和优化,设计并编写过一些 API, 每个程序员在做这个事情时,可能设计以及实现的思想都是不一样的,这些思想各有特点, 各有千秋,可以分为两大类:
第一类:着重对 JDBC 进行 API 层的抽取和封装,以及功能的增强,典型代表是 Apache 的
DbUtils。

程序员在使用 DbUtils 时仍然需要编写sql 语句并手动进行资料封装,但是 API 的使用比JDBC
方便了很多,下面是使用 DbUtils 的程式码片段:
@Test
public void testQuery(){
//建立 queryRunner 物件,用来操作 sql 语句
QueryRunner qr = new QueryRunner(JDBCUtils.getDataSource());
//编写 sql 语句
String sql = "select * from user";
//执行 sql 语句
try {
List list = qr.query(sql, new BeanListHandler(User.class)); System.out.println(list);
} catch (SQLException e) { e.printStackTrace();
}
}
第二类:借鉴面向物件的思想,让程序员以操作物件的方式操作数据库,无需编写 sql 语句, 典型代表是 ORM。ORM(Object Relational Mapping)吸收了面向物件的思想,把对 sql 的操作转换为物件的操作,从而让程序员使用起来更加方便和易于接受。这种转换是通过物件和表之间的元资料对映实现的,这是实现 ORM 的关键,如下图所示:

由于类和表之间以及属性和字段之间建立起了对映关系,所以,通过 sql 对表的操作就可以转换为物件的操作,程序员从此无需编写 sql 语句,由框架根据对映关系自动生成,这就是 ORM 思想。
目前比较流行的 Hibernate 和 MyBatis 都采用了 ORM 思想,一般我们把 Hibernate 称之为全自动的 ORM 框架,把 MyBatis 称之为半自动的 ORM 框架。使用过这两个框架的程序员, 对于 ORM 一定不会陌生。同时,ORM 也是 JPA(SUN 推出的持久层规范)的核心内容,如下图所示:

二.ORM 的经典应用:Hibernate
Hibernate 就是应用 ORM 思想建立的一个框架,一般我们把它称之为全自动的 ORM 框架,程序员在使用 Hibernate 时几乎不用编写 sql 语句,而是通过操作物件即可完成对数据库的增删改查。2.1Hibernate 案例
本次课程不是为了讲解 Hibernate 框架是如何使用的,而是想通过 Hibernate 框架让大家对 ORM 思想有一个更加深入的理解,接下来我们从一个案例开始:

1.pom.xml
cn.itcast.orm
HibernateDemo
1.0-SNAPSHOT
org.hibernate
hibernate-core
5.3.6.Final
junit
junit
4.12
mysql
mysql-connector-java
5.1.36
src/main/java
**/*.xml
false
src/main/resources
**/*.xml
false
2.Hibernate 核心配置档案 hibernate.cfg.xml
br>"-//Hibernate/Hibernate Configuration DTD//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
jdbc:mysql://localhost:3306/test
com.mysql.jdbc.Driver
root
123
update
org.hibernate.dialect.MySQLDialect
true
该配置档案主要设定了数据库连线资讯和对映配置档案的位置资讯
3.实体类 Book.java
public class Book {
private int id; //主键
private String name; //图书名字
private String author; //图书作者
private double price; //图书价格
... ...
}
这就是一个普通的实体类,描述和储存图书资讯
4.对映配置档案 Book.hbm.xml
该配置档案非常关键,重点体现了 ORM 思想,类和表之间,属性和字段之间的对映关
系清晰明了,当然现在也非常流行注解的方式,就是把对映关系以注解的方式放在实体类中, 如下所示:
@Entity
@Table(name = "t_book") public class Book {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY) //数据库自增@Column(name = "bid")
private Integer id; //主键
@Column(name="bname") private String name; //图书名字
@Column(name="author")
private String author; //图书作者
@Column(name="price")
private double price; //图书价格
... ...
}
不管你用 xml 配置的方式,还是注解的方式,其本质都是一样的,都是为了通过 ORM
思想把类和表之间,属性和字段之间的对映关系设定好。
5.测试类
public class BookTest {
private SessionFactory factory;
@Before
public void init(){
//1. 建立一个 Configuration 物件,解析 hibernate 的核心配置档案
Configuration cfg=new Configuration().configure();
//2. 建立 SessinFactory 物件,解析对映资讯并生成基本的 sql factory=cfg.buildSessionFactory();
}
@Test
public void testSave(){
//3. 得到 Session 物件,该物件具有增删改查的方法
Session session=factory.openSession();
//4. 开启事务
Transaction tx=session.beginTransaction();
//5. 储存资料
Book book=new Book(); book.setName("java 从入门到精通"); book.setAuthor(" 传 智 播 客 "); book.setPrice(9.9); session.save(book);
//6. 提交事务
tx.commit();
//7. 释放资源
session.close();
}
@Test
public void testGet(){
//3. 得到 Session 物件,该物件具有增删改查的方法
Session session=factory.openSession();
//4. 通过 Session 物件进行查询Book book=session.get(Book.class,2); System.out.println(book);
//5. 释放资源
session.close();
}
@Test
public void testDelete(){
//3. 得到 Session 物件,该物件具有增删改查的方法
Session session=factory.openSession();
//4. 开启事务管理
Transaction tx=session.beginTransaction();
//5. 删除资料
Book book=new Book(); book.setId(2); session.delete(book);
//6. 提交事务
tx.commit();;
//7. 释放资源
session.close();
}
}
该测试类使用 Hibernate 的 API 实现了图书的新增,查询和删除功能,程序员无需编写sql 语句,只需要像平时一样操作物件即可,然后由Hibernate 框架自动生成 sql 语句,如下图所示:

2.2Hibernate 的 ORM 实现原理
接下来我们通过上述案例来讲解一下 Hibernate 框架是如何应用 ORM 思想的,一起剖析一下 Hibernate 的内部实现原理。
其实不管使用什么框架,最终都需要生成 sql 语句,因为数据库需要的就是 sql 语句, 而我们在使用Hibernate 编码时没有编写sql 语句,只是提供了物件,那么 Hibernate 是如何根据物件生成sql 语句的呢?接下来我们一起跟踪并分析一下 Hibernate 5.x 的源代码。
1.Configuration cfg = new Configuration().configure();
1.1在 new Configuration()时,进行了 hibernate 的环境初始化工作,相关物件和容器被建立了出来,如下图所示:

红框中的两个物件大家要尤为注意,一个是第 158 行的 StandardServiceRegistryBuilder,
一个是第 161 行的 Properties 物件,后面的源代码中会重点用到这两个物件。
1.2接下来跟踪呼叫 configure()方法,如下图所示:

第 244 行的 configure(…)方法会预设载入名字为 hibernate.cfg.xml 的配置档案,然后去解析该配置档案并把解析到的资料存放到一个 Properties 中(第 258 和 261 行程式码)。解析出来的资料如下图所示:

通过上图大家能很清晰得看到,hibernate.cfg.xml 中的资讯被解析出来并存到了一个
Properties 中。
1.3我们再跟踪一下第 258 行的程式码,这行程式码呼叫了 StandardServiceRegistryBuilder
物件的 config 方法,如下图所示:

第 165 行从 hibernate.cfg.xml 中解析出来对映配置档案和实体类的资讯,并在第 178 行进行了合并汇总,最终储存到了 aggregatedCfgXml 中,如下图所示:

总之,第一步 Configuration cfg = new Configuration().configure(); 已经把 hibernate.cfg.xml
中的资讯全部解析了出来并进行了储存。
2.SessionFactory factory = cfg.buildSessionFactory();

由 Configuration 物件的 buildSessionFactory(…)方法建立会话工厂(SessionFactory),该
方法的程式码非常多,这里只截取了部分关键程式码:
第 723 行程式码从 Properties 中得到配置档案中的资料
第 653 行和第 689 行分别建立 MetadataBuilder 物件并呼叫该物件的 build()方法解析了对映资讯,然后储存到 Metadata 物件中,下列截图展示了从对映配置档案或实体类中解析出来的对映资料,包含哪个类和哪个表对应,哪个属性和哪个字段对应



第 708 行呼叫 SessionFatctoryBuilder 物件的 build()方法生成 sql 语句并返回
SessionFactory 例项,下面截图展示出了生成的 sql 语句:

会话工厂(SessionFactory)在 Hibernate 中实际上起到了一个缓冲区的作用,它快取了Hibernate 自动生成的 SQL 语句和相关对映资料,只要生成了 sql 语句,那么后面实现增删改查就不在话下。
三.ORM 的经典应用:MyBatis
MyBatis 框架也应用了ORM 思想,一般我们把它称之为半自动的 ORM 框架,跟Hibernate 相比,MyBatis 更加轻量,更加灵活,为了保证这一点,程序员在使用 MyBatis 时需要自己编写 sql 语句,但是 API 的使用依然像Hibernate 一样简单方便。3.1MyBatis 案例
本次课程不是为了讲解 MyBatis 框架是如何使用的,而是想通过 MyBatis 框架让大家对
ORM 思想有一个更加深入的理解,接下来我们从一个案例开始:

1.pom.xml
cn.itcast.orm
MyBatisDemo
1.0-SNAPSHOT
org.mybatis
mybatis
3.4.0
mysql
mysql-connector-java
5.1.36
junit
junit
4.12
src/main/java
**/*.xml
false
src/main/resources
**/*.xml
false
2.MyBatis 核心配置档案 SqlMapConfig.xml
br>PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
该配置档案主要设定了数据库连线资讯和对映配置档案的位置资讯
3.实体类 DeptEmp.java
// 实体类:储存各部门的员工总数
public class DeptEmp {
private String deptName; //部门名称private int total; //员工总数
... ...
}
// 实体类:储存各部门的员工总数
public class DeptEmp {
private String deptName; //部门名称private int total; //员工总数
… …
}
//Mapper 界面
public interface DeptEmpMapper {
//查询各部门的员工总数List getEmpTotalByDept();
}
这是 Mapper 界面,我们只需要定义方法,由 MyBatis 框架建立代理类(实现类)并实现功能
4.对映配置档案 DeptEmpMapper.xml
br>PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
select dname, count(*) as total from dept,emp where emp.dept_id=dept.did group by dname
MyBatis 其实会采用预设规则自动建立表和实体类之间,属性和字段之间的对映关系,但是
由于我们这个案例中是要查询各部门的员工总数,无法自动对映,所以我们自定义了一个resultMap 来配置实体类和查询结果之间的对映关系,想通过这个案例更加明显的体现一下ORM 思想。
5.测试类
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 list = deptEmpMapper.getEmpTotalByDept(); for (DeptEmp de : list) {
System.out.println(de);
}
//关闭 sqlSession sqlSession.close();
}
}
该测试类通过MyBatis 的 API 实现了查询功能,如下图所示:

3.2MyBatis 的 ORM 实现原理
接下来我们通过上述案例来讲解一下 MyBatis 框架是如何应用 ORM 思想的,一起剖析一下 MyBatis 3.x 的内部实现原理。
1.解析 MyBatis 核心配置档案并建立 SqlSessionFactory 物件
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
这三行程式码先是建立了 SqlSessionFactoryBuilder 物件,然后获得了 MyBatis 核心配置档案的输入流,最后呼叫了 build 方法,我们接下来跟踪一下该方法的源代码。

第 69 行程式码建立了一个xml 解析器物件,并在第 70 行对MyBatis 核心配置档案进行了解析, 拿到了数据库连线资料以及对映配置档案中的资料(包括我们编写的 sql 语句和自定义的resultMap),如下图所示:


第 88 行程式码建立了 DefaultSqlSessionFactory 物件,并把上图中展示的解析后拿到的资料传给了它,我们继续跟踪。

在 DefaultSqlSessionFactory 的构造方法中,我们发现,解析后拿到的资料最终被全部存入到了一个名字为 Configuration 的物件中,后续很多操作都会从该物件中获取资料。
2.SqlSession sqlSession = sqlSessionFactory.openSession();

图中红框部分程式码,主要通过读取 Configuration 物件中的资料分别建立了 Environment 物件, 事务物件,Executor 物件等,并最终直接 new 了一个 DefaultSalSession 物件(SqlSession 界面的实现类),该物件是MyBatis API 的核心物件。
3.DeptEmpMapper deptEmpMapper = sqlSession.getMapper(DeptEmpMapper.class);

第 269 行程式码呼叫了 Configuration 物件的 getMapper 方法,把实体类和 sqlSession 物件传了过去。

第 693 行程式码呼叫了 MapperRegistry 物件的 getMapper 方法,把实体类和 sqlSession 物件传了过去。

第 34 行程式码通过代理工厂最终把我们自定义的 Mapper 界面的代理物件建立了出来。
4.List list = deptEmpMapper.getEmpTotalByDept();

当我们呼叫代理物件的 getEmpTotalByDept()方法时,框架内部会呼叫 MapperProxy 的 invoke
方法, 我们可以观察一下这个方法三个引数的值,如下图所示:

在 invoke 方法内锁定第 36 行程式码,该行程式码呼叫 MapperMethod 的 execute 方法实现查询功能。

在 execute 方法内先进行增删改查判断,本案例进行的是查询,并且可能会查询出多条记录, 所以最后锁定第 60 行程式码,该行程式码呼叫executeForMany 方法进行查询。

第 124 行程式码通过 hasRowBounds()方法判断是否进行分页查询,本案例不需要,所以执行else 中的程式码,呼叫 sqlSession 的 selectList 方法(第 128 行), 引数的值如下图所示:


第 123 行程式码从 Configuration 物件中获得 MappedStatement 物件,该物件储存了对映配置档案中的所有资讯(包括我们编写的 sql 语句和自定义的resultMap),如下图所示:

第 124 行程式码呼叫了 CachingExecutor 的 query(…)方法, 继续往下跟踪。

第 61 行通过 ms 物件从 mapper 对映配置档案中获得了 sql 语句,如下图所示:

第 63 行程式码呼叫了下面的 query 方法,该方法内部先从快取中取资料,如果快取中没资料, 就重新查询(第 87 行),继续往下跟踪。

第 134 行程式码通过呼叫 queryFromDatabase 方法最终执行了 sql 语句并将查询结果按照我们
自定义的resultMap 进行了封装,结果如下图所示:
