《深入理解mybatis原理》MyBatis的架构设计以及实例分析转载

原创
小哥 3年前 (2022-10-27) 阅读数 5 #大杂烩

MyBatis它目前很受欢迎。ORM框架,它非常强大,但它的实现相对简单和优雅。本文的重点是MyBatis建筑设计理念,并讨论MyBatis几个核心组件,然后将一个select查询实例,钻取代码,浏览。MyBatis的实现。

一、MyBatis框架设计
注:以上数字为大体上参考。iteye 上的chenjc_it   博客文章写作的第二个原则分析:框架的总体设计 中的MyBatis建筑主体图,chenjc_it总结得很好,表扬一声!

1.接口层---以及数据库交互的方式
MyBatis与数据库交互的方式有两种:

a.使用传统的MyBatis提供的API;

b. 使用Mapper接口

1.1.使用传统的MyBatis提供的API

这是传统的传输方式Statement Id 和查询参数 SqlSession 对象,使用 SqlSession对象完成和数据库交互;MyBatis 提供了一种非常方便和简单的API实现了对数据库中数据的添加和删除操作,以及数据库的连接信息和。MyBatis 自我配置信息的维护操作。

上述使用MyBatis 方法,就是创建一个数据库来处理。SqlSession对象,然后根据Statement Id 和参数一起操作数据库,这种方式很简单实用,但它不符合面向对象语言的概念和面向接口的编程习惯。由于面向接口编程是面向对象的大趋势,MyBatis 为了适应这一趋势,增加了第二种用途MyBatis 支持界面(Interface)调用方法。

1.2. 使用Mapper接口

MyBatis 每个配置文件都将是 节点被抽象为 Mapper 接口,以及在此接口和下面的接口中声明的方法 节点中的<select|update|delete|insert> 节点项对应,即<select|update|delete|insert> 节点的id值为Mapper 接口中的方法名称,parameterType 值表示Mapper 对应方法的条目类型,以及resultMap 值对应Mapper 由返回结果集的接口或元素类型表示的返回值类型。

根据MyBatis 在完成配置规范后SqlSession.getMapper(XXXMapper.class) 方法,MyBatis 基于由相应接口声明的方法信息来生成动态代理机制。Mapper 实例,我们使用Mapper 当接口的方法时,MyBatis 将根据此方法的方法名称和参数类型确定。Statement Id、底层或通透SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject); 等实现对数据库的操作,(关于这里的动态机制是如何实现的,我会准备一篇专门的文章来讨论,请注意~)

MyBatis 引用Mapper 调用该接口纯粹是为了满足面向接口编程的需要。(事实上,另一个原因是面向接口的编程允许用户在接口上使用注释来配置SQL语句,这样您就可以脱离XML配置文件,实施“0配置“)。

2.数据处理层
数据处理层可以说是MyBatis 处于……的核心

a. 通过传入参数来构建动态。SQL语句;

b. SQL语句执行和封装查询结果的集成。List
2.1.参数映射和动态SQL语句生成

动态语句生成可以说是MyBatis框住了一个非常优雅的设计,MyBatis 通过传入参数值,使用。 Ognl 动态构建SQL语句,以便MyBatis 具有较强的灵活性和可扩展性。

参数映射指的是java 数据类型和jdbc数据类型之间的转换:有两个过程:查询阶段,我们会。java数据类型,已转换jdbc数据类型, preparedStatement.setXXX() 设定值;resultset查询结果集jdbcType 数据转换java 数据类型。

(至于具体情况MyBatis是如何动态构建SQL我将准备一篇专题文章来讨论这一声明,请注意。~)

2.2. SQL语句执行和封装查询结果的集成。List

动态SQL在生成该语句之后,MyBatis 将执行SQL语句,并将可能返回的结果集转换为List 列表。MyBatis 在结果集处理中,支持结果集关系的一对多和多对一转换,支持方式有两种,一种是对嵌套查询语句的查询,一种是对嵌套结果集的查询。

  1. 框架支撑层
    3.1. 交易管理机制

交易管理机制对于ORM框架而言是不可缺少的一部分,交易管理机制的质量也是考量一个ORM一个框架是否优秀的标准,对于数据管理机制,我在我的博客文章《深入理解》中有过。mybatis原理》 MyBatis交易管理机制 有一个非常详细的讨论,感兴趣的读者可以点击查看。

3.2. 连接池管理机制

因为创建数据库连接会占用大量资源, 对于数据吞吐量大、流量非常大的应用,连接池的设计是非常重要的。我已经在我的博客文章《深入理解》中了解了连接池管理机制。mybatis原理》 Mybatis数据源和连接池 有一个非常详细的讨论,感兴趣的读者可以点击查看。

3.3. 缓存机制

为了提高数据利用率并减轻服务器和数据库的压力,MyBatis 为某些查询提供会话级数据缓存,并将查询放在SqlSession 在中,对于允许的间隔内的完全相同的查询,MyBatis 会直接将缓存结果返回给用户,而不用再到数据库中查找。(至于具体情况MyBatis缓存机制,我会准备一篇专题文章来讨论,请大家注意~)

    1. SQL语句的配置方式

传统的MyBatis 配置SQL 语句方法是使用XML文件是配置的,但这种方法不能很好地支持面向接口编程的概念,为了支持面向接口编程,MyBatis 引入了Mapper接口的概念,引入面向接口,使用注释进行配置SQL 声明是可能的。用户只需要在界面上添加必要的注释,而不需要进行配置。XML文件,但当前的MyBatis 只需配置注释即可SQL 语句提供的支持有限,一些高级功能仍依赖于XML配置文件配置SQL 语句。

4 引导层
引导层已配置并启动。MyBatis 信息是如何配置的。MyBatis 引导的两种方式MyBatis :基于XML简档的方式和基于的。Java API 读者可以参考我的另一篇博客文章:Java Persistence with MyBatis 3(中文版) 第二章 引导MyBatis

二、MyBatis的主要组件
从MyBatis从代码实现的角度来看,MyBatis主要核心组件如下:

SqlSession            作为MyBatis工作的主要顶层API表示与数据库交互并完成必要的数据库添加、删除和修改功能的会话
Executor              MyBatis执行机构,ISMyBatis 调度核心,有责任感SQL语句生成和查询缓存维护。
StatementHandler   封装了JDBC Statement运营,负责JDBC statement 操作,如设置参数,将。Statement转换结果集List集合。
ParameterHandler   负责传递给用户的参数被转换JDBC Statement 必填参数,
ResultSetHandler    负责将JDBC返回的ResultSet转换结果集对象。List类型集合;
TypeHandler          负责java数据类型和jdbc数据类型之间的映射和转换
MappedStatement   MappedStatement保持为<select|update|delete|insert>节点的封装,
SqlSource            负责用户的交付。parameterObject,动态生成SQL语句,封装信息BoundSql对象,并返回
BoundSql             表示动态生成的SQL语句和相应的参数信息
Configuration        MyBatis维护所有配置信息Configuration物体。
(注:这只是我个人认为属于核心的组件的列表。请不要有先入为主的想法MyBatis只有这几个部分!每个人都是对的。MyBatis理解不同,分析的结果自然也会不同,欢迎读者提出问题和不同的意见,我们一起探讨。~)

它们之间的关系如下图所示:

三、从MyBatis一次select 要分析的查询语句MyBatis建筑设计
1.数据准备(非常熟悉和实用MyBatis 读者可以快速浏览这一部分)

  1. 准备数据库数据,创建EMPLOYEES表格,插入数据:

--创建员工基本信息表。
create  table "EMPLOYEES"(
"EMPLOYEE_ID" NUMBER(6) not null,
"FIRST_NAME" VARCHAR2(20),
"LAST_NAME" VARCHAR2(25) not null,
"EMAIL" VARCHAR2(25) not null unique,
"SALARY" NUMBER(8,2),
constraint "EMP_EMP_ID_PK" primary key ("EMPLOYEE_ID")
);
comment on table EMPLOYEES is 员工信息表;
comment on column EMPLOYEES.EMPLOYEE_ID is 员工id;
comment on column EMPLOYEES.FIRST_NAME is first name;
comment on column EMPLOYEES.LAST_NAME is last name;
comment on column EMPLOYEES.EMAIL is email address;
comment on column EMPLOYEES.SALARY is salary;

--添加数据
insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY)
values (100, Steven, King, SKING, 24000.00);

insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY)
values (101, Neena, Kochhar, NKOCHHAR, 17000.00);

insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY)
values (102, Lex, De Haan, LDEHAAN, 17000.00);

insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY)
values (103, Alexander, Hunold, AHUNOLD, 9000.00);

insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY)
values (104, Bruce, Ernst, BERNST, 6000.00);

insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY)
values (105, David, Austin, DAUSTIN, 4800.00);

insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY)
values (106, Valli, Pataballa, VPATABAL, 4800.00);

insert into EMPLOYEES (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY)
values (107, Diana, Lorentz, DLORENTZ, 4200.00);

  1. 配置Mybatis的配置文件,名为mybatisConfig.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">

3.     创建Employee实体Bean 以及配置Mapper配置文件

package com.louis.mybatis.model;

import java.math.BigDecimal;

public class Employee {
private Integer employeeId;

private String firstName;

private String lastName;

private String email;

private BigDecimal salary;

public Integer getEmployeeId() {
return employeeId;
}

public void setEmployeeId(Integer employeeId) {
this.employeeId = employeeId;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public BigDecimal getSalary() {
return salary;
}

public void setSalary(BigDecimal salary) {
this.salary = salary;
}
}

<?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" >

  1. 创建eclipse 或者myeclipse 的maven项目,maven配置如下:

<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">

4.0.0 batis batis 0.0.1-SNAPSHOT jar batis http://maven.apache.org UTF-8 junit junit 3.8.1 test org.mybatis mybatis 3.2.7 com.oracle ojdbc14 10.2.0.4.0

  1. 客户端代码:

package com.louis.mybatis.test;

import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import com.louis.mybatis.model.Employee;

/**

  • SqlSession 简单查询演示类
  • @author louluan
    */
    public class SelectDemo {

public static void main(String[] args) throws Exception {
/*

  • 1.加载mybatis配置文件的初始化mybatis,创建出SqlSessionFactory,是创建SqlSession的工厂
  • 这仅用于演示目的,SqlSessionFactory临时创建,并在实际使用中,SqlSessionFactory您只需创建一次并将其用作单个案例。
    */
    InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml");
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    SqlSessionFactory factory = builder.build(inputStream);

//2. 从SqlSession工厂 SqlSessionFactory创建SqlSession、数据库操作
SqlSession sqlSession = factory.openSession();

//3.使用SqlSession查询
Map<String,Object> params = new HashMap<String,Object>();

params.put("min_salary",10000);
//a.询价工资较低10000的员工
List result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);
//b.没有超过最低工资标准,请检查所有员工
List result1 = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary");
System.out.println("薪资低于10000员工的比例:"+result.size());
//~output :   查询数据总数:5
System.out.println("所有员工人数: "+result1.size());
//~output :  所有员工人数: 8
}

}

二、SqlSession 工作流程分析:

  1. 打开数据库访问会话---创建SqlSession对象:
    SqlSession sqlSession = factory.openSession();
    MyBatis封装对数据库的访问,并对数据库进行会话和事务控制。SqlSession对象中。

  2. 为SqlSession传递已配置的Sql语句 的Statement Id和参数,然后返回结果:
    List result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);
    上述的"com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",配置在EmployeesMapper.xml 的Statement ID,params 是传递的查询参数。
    让我们来看看sqlSession.selectList()方法定义:

public List selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

public List selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//1.根据Statement Id,在mybatis 配置对象Configuration在以下位置找到相应的配置文件MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//2. 委托查询任务MyBatis 的执行器 Executor
List result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

MyBatis在初始化时,MyBatis配置信息全部加载到内存中,使用org.apache.ibatis.session.Configuration要维护的实例。用户可以使用sqlSession.getConfiguration()方法获取。MyBatis配置文件中配置信息的组织格式与内存中对象的组织格式几乎完全一致。在上面的例子中

加载到内存中会生成对应的MappedStatement对象,该对象后跟一个key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary" ,value为MappedStatement对象的形状保持不变。Configuration的一个Map在……里面。当它需要在未来使用时,只能通过Id获得它的价值。
从上面的代码中我们可以看到SqlSession功能:

SqlSession根据Statement ID, 在mybatis配置对象Configuration相应的MappedStatement对象,然后调用mybatis执行特定操作的执行器。

3.MyBatis执行器Executor根据SqlSession将执行传递的参数。query()方法(因为代码太长,读者只需要阅读我注释的地方):
/**

  • BaseExecutor 类部分代码
  • */
    public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

// 1.根据具体传入的参数,动态生成需要执行的SQL语句,用BoundSql对象表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 2.为当前查询创建缓存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@SuppressWarnings("unchecked")
public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) throw new ExecutorException("Executor was closed.");
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List list;
try {
queryStack++;
list = resultHandler == null ? (List) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 3.缓存中没有值,数据直接从数据库中读取。
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear(); // issue #601
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
}
return list;
}
private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {

//4. 执行查询并返回List 结果,然后    将查询结果放入缓存。
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

/**

  • *SimpleExecutor类的doQuery()方法实现

  • */
    public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
    Configuration configuration = ms.getConfiguration();
    //5. 根据已有参数,创建StatementHandler对象来执行查询操作。
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    //6. 创建java.Sql.Statement对象,传递StatementHandler对象
    stmt = prepareStatement(handler, ms.getStatementLog());
    //7. 调用StatementHandler.query()方法,返回List结果集
    return handler.query(stmt, resultHandler);
    } finally {
    closeStatement(stmt);
    }
    }

上述的Executor.query()该方法反复执行几次,最终创建了一个StatementHandler对象,然后将必要的参数传递给StatementHandler,使用StatementHandler完成对数据库的查询并最终返回List结果集。
正如我们从上面的代码中看到的,Executor以下机构的职能和职能:

(1,根据传递的参数,完成。SQL动态解析语句,生成BoundSql对象,供StatementHandler使用;

(2,为查询创建缓存以提高性能(具体的缓存机制不是本文的重点,我将单独拿出来与您讨论,感兴趣的读者可以关注我的其他博客帖子);

(3、创建JDBC的Statement连接对象,传递StatementHandler对象,返回List查询结果。

  1. StatementHandler对象负责设置Statement查询对象中的参数,处理JDBC返回的resultSet,将resultSet加工为List 集合退货:
    然后是上面的Executor第六步,看一看:prepareStatement() 该该该方法的实现:

/**

  • *SimpleExecutor类的doQuery()方法实现

  • */
    public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 1.准备Statement对象,并设置Statement对象的参数 stmt = prepareStatement(handler, ms.getStatementLog()); // 2. StatementHandler执行query()方法,返回List结果 return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection);
//对创建的Statement对象设置参数,即设置SQL 语句中 ? 设置为指定的参数
handler.parameterize(stmt);
return stmt;
}

以上我们可以总结一下StatementHandler该对象主要完成两个任务:

(1. 对于JDBC的PreparedStatement对象的类型,创建过程,我们使用SQL语句字符串将包含 若干个? 占位符,然后我们设置占位符的值。

StatementHandler通过parameterize(statement)方法对Statement设置值;

(2.StatementHandler通过List query(Statement statement, ResultHandler resultHandler)方法来完成执行。Statement,和将Statement该对象返回resultSet封装成List;

5.   StatementHandler 的parameterize(statement) 该该该方法的实现:
/**

  •   StatementHandler 类的parameterize(statement) 方法实现
    */
    public void parameterize(Statement statement) throws SQLException {
    //使用ParameterHandler对象来完成配对。Statement的设值
    parameterHandler.setParameters((PreparedStatement) statement);
    }

/**

  • *ParameterHandler类的setParameters(PreparedStatement ps) 实现

  • 对某一个Statement设置参数
    */
    public void setParameters(PreparedStatement ps) throws SQLException {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
    for (int i = 0; i < parameterMappings.size(); i++) {
    ParameterMapping parameterMapping = parameterMappings.get(i);
    if (parameterMapping.getMode() != ParameterMode.OUT) {
    Object value;
    String propertyName = parameterMapping.getProperty();
    if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
    value = boundSql.getAdditionalParameter(propertyName);
    } else if (parameterObject == null) {
    value = null;
    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
    value = parameterObject;
    } else {
    MetaObject metaObject = configuration.newMetaObject(parameterObject);
    value = metaObject.getValue(propertyName);
    }

// 每一个Mapping都有一个TypeHandler,根据TypeHandler来对preparedStatement设置参数
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
// 设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
正如您从上面的代码中看到的那样,StatementHandler 的parameterize(Statement) 该方法调用 ParameterHandler的setParameters(statement) 方法,

ParameterHandler的setParameters(Statement)方法负责 根据我们输入的参数,这对statement对象的 ? 该值在占位符处赋值。

6.   StatementHandler 的List query(Statement statement, ResultHandler resultHandler)该该该方法的实现:
/**