简介
传统的JDBC编程
Java数据库连接,(Java Database Connectivity,简称JDBC)是Java中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。
Connection conn = null;
Statement stmt = null;
try {
//1: 加载类驱动
Class.forName("com.mysql.jdbc.Driver");
//2: 打开连接
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/demo", "test", "test");
//3: 执行sql,返回结果
stmt = conn.createStatement();
String sql = "SELECT id, first, last, age FROM Registration";
ResultSet rs = stmt.executeQuery(sql);
//4: 对返回结果集处理
while (rs.next()) {
int id = rs.getInt("id");
}
// 释放资源
rs.close();
} catch (Exception e) {
} finally {
try {
if (stmt != null) {
conn.close();
}
} catch (SQLException se) {}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException se) {}
}
整个过程大致分为以下几步:
- 注册数据库驱动类,明确指定数据库URL地、数据库用户名、密码等连接信息。
- 通过DriverManager 打开数据库连接。
- 通过 Statement 对象执行SQL语句,得到ResultSet 对象。
- 通过 ResultSet 读取数据,并将数据转换成JavaBean 对象
- 关闭 ResultSet、 Statement 对象以及数据库连接,释放相关资源。
JDBC方式存在弊端。其一,工作量相对较大。我们需要先连接,然后处理JDBC底层事务,处理数据类型。我们还需要操作Connection对象、Statement对象和ResultSet对象去拿到数据,并准确关闭它们。其二,我们要对JDBC编程可能产生的异常进行捕捉处理并正确关闭资源。
ORM模型
由于JDBC存在的缺陷,在实际工作中我们很少使用JDBC进行编程,于是提出了对象关系映射(Object Relational Mapping,简称ORM,或者O/RM,或者O/R mapping)。ORM模型就是数据库的表和简单Java对象(Plain Ordinary Java Object,简称POJO)的映射关系模型,它主要解决数据库数据和POJO对象的相互映射。
Hibernate
Hibernate是建立在若干POJO通过XML映射文件(或注解)提供的规则映射到数据库表上的。可以通过POJO直接操作数据库的数据。它提供的是一种全表映射的模型。Hibernate对JDBC的封装程度还是比较高的,我们不需要编写SQL 语言,只要使用HQL语言(Hibernate Query Langurage)就可以了。
与MyBatis 区别
Hibernate编程简易,需要我们提供映射的规则,完全可以通过IDE生成,同时无需编写SQL确实开发效率优于MyBatis。提供了缓存、日志、级联等强大的功能,但是Hibermate的缺陷也是十分明显的,多表关联复杂 SQL,数据系统权限限制,根据条件变化的 SQL。存储过程等场景使用Hibernate 十分不便,而性能又难以通过SQL 优化。所以注定了 Hibenate 只适用于在场景不太复杂,要求性能不太苛刻的时候使用。 MyBatis 具有高度灵活、可优化、易维护等特点,所以它目前是大型移动互联网项目的首选框架。
MyBatis 整体架构
MyBatis的整体架构分为三层,分别是基础支持层、核心处理层和接口层
基础支持层
基础支持层位于MyBatis整体架构的最底层,支撑着MyBatis的核心处理层,是整个框架的基石。基础支持层中封装了多个较为通用的、独立的模块,不仅仅为MyBatis提供基础支撑,也可以在合适的场景中直接复用。
核心处理层
核心处理层以基础支持层为基础,实现了MyBatis的核心功能。包括MyBatis的初始化、动态SQL语句的解析、结果集的映射、参数解析以及SQL语句的执行等几个方面的流程。
接口层
SqlSession是MyBatis核心接口之一,也是MyBatis接口层的主要组成部分,对外提供MyBatis 常用API,也就是上层应用与MyBatis交互的桥梁。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作。
Mybatis基本原理
读取MyBatis配置文件
MyBatis初始化的主要工作是加载并解析mybatis-config.xml配置文件、映射配置文件以及相关的注解信息。MyBatis的初始化入口是SqlSessionFactoryBuilder .build()方法,如下:
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
SqlSessionFactory var5;
try {
//读取配置文件
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//解析配置文件得到Configuration对象,创建DefaultsqlsessionFactory对象
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
//...关闭读取配置文件的输入流对象(略)
}
return var5;
}
}
SqlSessionFactoryBuilder会通过XMLConfigBuilder 等对象读取XML配置文件以及映射配置信息,得到Configuration对象,然后创建 SqlSessionFactory对象。
加载映射文件
XMLMapperBuilder.parse()方法是解析映射文件的入口,具体代码如下:
public class XMLMapperBuilder extends BaseBuilder {
public void parse() {
//判断是否已经加载过该映射文件
if (!this.configuration.isResourceLoaded(this.resource)) {
//处理<mapper>节点
this.configurationElement(this.parser.evalNode("/mapper"));
//将resource添加到Configuration.loadedResources集合中保存,
// 它是Hashset<String>/类型的集合,其中记录了已经加载过的映射文件。
this.configuration.addLoadedResource(this.resource);
//注册mapper接口
this.bindMapperForNamespace();
}
//处理configurationElement()方法中解析失败的<resultMap>节点
this.parsePendingResultMaps();
//处理configurationElement()方法中解析失败的<cache-ref>节点
this.parsePendingCacheRefs();
//处理configurationElement()方法中解析失败的SQL语句节点
this.parsePendingStatements();
}
}
解析XML映射文件SQL节点
映射配置文件中还有一类比较重要的节点需要解析,就是SQL节点,主要用于定义SQL语句,由XMLStatementBuilder.parseStatementNode()方法负责进行解析。映射配置文件中定义的 SQL节点会被解析成MappedStatement对象,其中的SQL语句会被解析成SqLSource对象,SQL语句中定义的动态SQL节点、文本节点等,则由SqlNode接口的相应实现表示。
public class XMLStatementBuilder extends BaseBuilder {
public void parseStatementNode() {
//获取SQL节点的id以及 databaseId属性
String id = this.context.getStringAttribute("id");
String databaseId = this.context.getStringAttribute("databaseId");
if (this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
// 根据SQL节点的名称决定其 Sq1CommandType:SELECT/DELETE...
String nodeName = this.context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 在解析SQL语句之前,先处理其中的<include>节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this.configuration, this.builderAssistant);
includeParser.applyIncludes(this.context.getNode());
//...获取SQL节点的多种属性值,例如、parameterType、parameterMap、
String parameterType = this.context.getStringAttribute("parameterType");
//...处理<selectKey>节点
this.processSelectKeyNodes(id, parameterTypeClass, langDriver);
//完成SQL解析....
//通过 MapperBui1derAssistant 创建 Mappedstatement 对象
this.builderAssistant.addMappedStatement()
}
}
}
绑定 Mapper接口
在XMLMapperBuilder.bindMapperForNamespace()方法中,完成了映射配置文件与对应 Mapper 接口的绑定,如下:
public class XMLMapperBuilder extends BaseBuilder {
private void bindMapperForNamespace() {
//获取映射配置文件的命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class boundType = null;
try {
//解析命名空间对应的类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException var4) {
}
//是否已经加载了 boundType接口
if (boundType != null && !this.configuration.hasMapper(boundType)) {
//追加 namespace前缀,并添加到集合中保存
this.configuration.addLoadedResource("namespace:" + namespace);
//调用MapperRegistry.addMapper()方法,注册boundType接口
this.configuration.addMapper(boundType);
}
}
}
}
创建SqlSession对象
SqlSession 是一个会话,相当于 JDBC 的一个Connection 对象,它的生命周期应该是在请求数据库处理事务的过程中。SqlSessionFactory负责创建SqlSession对象,SqlSession 对象实现都是 DefaultSqlSession,它也是单独使用MyBatis 进行开发时最常用的 SqlSession接口实现。
public class DefaultSqlSession implements SqlSession {
//Configuration配置对象
private final Configuration configuration;
//底层依赖的 Executor 对象
private final Executor executor;
//是否自动提交事务
private final boolean autoCommit;
.............
}
DefaultSqlSessionFactory
一个具体工厂类,实现了 SqlSessionFactory 接口。 DefaultSqlSessionFactory 主要提供了两种创建DefaultSqlSession对象的方式
- 通过数据源获取数据库连接,并创建Executor对象以及DefaultSqlSession对象;
- 用户提供数据库连接对象,DefaultSq]SessionFactory会使用该数据库连接对象创建Executor 对象以及DefaultSqlSession对象。
SqlSessionManager
SqlSessionManager同时实现了SqlSession 接口和 SqlSessionFactory 接口,也就同时提供了 SqlSessionFactory创建SqlSession对象以及 SqlSession操纵数据库的功能。
SqlSessionManager 与 DefaultSqlSessionFactory的主要不同点是SqlSessionManager提供了两种模式:第一种模式与DefaultSqlSessionFactory 的行为相同,同一线程每次通过SqlSessionManager 对象访问数据库时,都会创建新的DefaultSession 对象完成数据库操作:第二种模式是SqlSessionManager 通过localSqlSession 这个 ThreadLocal 变量,记录与当前线程绑定的SqlSession 对象,供当前线程循环使用,从而避免在同一线程多次创建 SqlSession 对象带来的性能损失。
Executor执行器
Executor 是 MyBatis 的核心接口之一,其中定义了数据库操作的基本方法。在实际应用中经常涉及的 SqlSession接口的功能,都是基于Executor 接口实现的。它来调度 StatementHandler、 ParameterHandler、ResultHandler 等来执行对应的 SQL。
StatementHandler
StatementHandler接口中的功能很多,例如创建 Statement 对象,为SQL语句绑定实参,执行 select、 insert、 update、 delete等多种类型的 SQL语句,批量执行 SQL语句,将结果集映射成结果对象。
MyBatis 根据Configuration来构建 StatementHandler,然后使用prepareStatement方法,对SQL编译并对参数进行初始化,调用了StatementHandler的 prepare()进行了预编译和基础设置,然后通过 StatementHandler 的 parameterize()来设置参数并执行。
public interface StatementHandler {
//从连接中获取一个Statement
Statement prepare(Connection var1, Integer var2) throws SQLException;
//绑定statement 执行时所需的实参
void parameterize(Statement var1) throws SQLException;
//批量执行SQL语句
void batch(Statement var1) throws SQLException;
//执行 update/insert/delete语句
int update(Statement var1) throws SQLException;
//执行select语句
<E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;
<E> Cursor<E> queryCursor(Statement var1) throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
ParameterHandler参数映射
MyBatis是通过参数处理器(ParameterHandler)对预编译语句进行参数设置的。它的作用是很明显的,那就是完成对预编译参数的设置。
public class DefaultParameterHandler implements ParameterHandler {
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId());
//取出sql参数映射列表
List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); ++i) {
ParameterMapping parameterMapping = (ParameterMapping) parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
//获取参数名称
String propertyName = parameterMapping.getProperty();
Object value;
//获取对应实参值
if (this.boundSql.hasAdditionalParameter(propertyName)) {
value = this.boundSql.getAdditionalParameter(propertyName);
} else if (this.parameterObject == null) {
value = null;
} else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {
//实参可以直接通过 Typehandler转换成JdbcType
value = this.parameterObject;
} else {
MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);
value = metaObject.getValue(propertyName);
}
//获取对象中相应的属性值或查找Map对象中值
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = this.configuration.getJdbcTypeForNull();
}
try {
//为SQL语句绑定相应的实参
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (SQLException | TypeException var10) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10);
}
}
}
}
}
}
为SQL语句绑定完实参之后,就可以调用 Statement对象相应的execute()方法,将SQL语句交给数据库执行了。
ResultSetHandler结果映射
MyBatis将结果集按照映射配置文件中定义的映射规则,例如节点、resultType 属性等,映射成相应的结果对象。这种映射机制是MyBatis的核心功能之一,可以避免重复的 JDBC 代码。 在 StatementHandler 接口在执行完指定的 select 语句之后,会将查询得到的结果集交给ResultSetHandler 完成映射处理。ResultSetHandler除了负责映射 select 语句查询得到的结果集。
通过 select 语句查询数据库得到的结果集由 DefaultResultSetHandler.handleResultSets()方法进行处理,该方法处理 Statement、 PreparedStatement 产生的结果集。
public class DefaultResultSetHandler implements ResultSetHandler {
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(this.mappedStatement.getId());
//该集合用于保存映射结果集得到的结果对象
List<Object> multipleResults = new ArrayList();
int resultSetCount = 0;
//获取第一个ResultSet对象,可能存在多个ResultSet,这里只获取第一个Resultset
ResultSetWrapper rsw = this.getFirstResultSet(stmt);
//获取 Mappedstatement.resultMaps集合,
// 映射文件中的 <resultMap>节点会被解析成ResultMap对象,保存到集合中
List<ResultMap> resultMaps = this.mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
this.validateResultMapsCount(rsw, resultMapCount);
while(rsw != null && resultMapCount > resultSetCount) {
//获取该结果集对应的ResultMap对象
ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount);
//根据 ResultMap中定义的映射规则对ResultSet进行映射,
// 并将映射的结果对象添加到multipleResults集合中保存
this.handleResultSet(rsw, resultMap, multipleResults, (ResultMapping)null);
rsw = this.getNextResultSet(stmt);
this.cleanUpAfterHandlingResultSet();
++resultSetCount;
}
.....
return this.collapseSingleResultList(multipleResults);
}
}
mybatis缓存原理
MyBatis中通过Executor负责维护一级缓存和二级缓存。
一级缓存
一级缓存是会话级别的缓存,在MyBatis 中每创建一个SqlSession对象,就表示开启一次数据库会话。在一次会话中,应用程序可能会在短时间内,例如一个事务内,反复执行完全相同的查询语句,如果不对数据进行缓存,那么每一次查询都会执行一次数据库查询操作,而多次完全相同的、时间间隔较短的查询语句得到的结果集极有可能完全相同,这也就造成了数据库资源的浪费。 MyBatis 默认情况下一级缓存是开启的。
通过BaseExecutor管理一级缓存
public abstract class BaseExecutor implements Executor {
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//创建CacheKey对象
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
}
CacheKey对象由MappedStatement 的id、对应的offset 和 limit、 SqL语句(包含“?”占位符)、用户传递的实参以及Environment的id这五部分构成。缓存使用的数据结构是一个map。
二级缓存
MyBatis中提供的二级缓存是应用级别的缓存,它的生命周期与应用程序的生命周期相同。 二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis要求返回的 POJO 必须是可序列化的,也就是要求实现Serializable接口,在映射XML文件配置或者通过注解@CacheNamespace开启缓存。
二级缓存是通过 CachingExecutor实现的,CachingExecutor.query()方法执行查询操作的步骤
(1)获取BoundSql对象,创建查询语句对应的CacheKey对象。
(2)检测是否开启了二级缓存,如果没有开启二级缓存,则直接调用底层Executor对象的query()方法查询数据库。如果开启了二级缓存,则继续后面的步骤。
(3)检测查询操作是否包含输出类型的参数,如果是这种情况,则报错。
(4)调用TransactionalCacheManager.getObject()方法查询二级缓存,如果二级缓存中查找到相应的结果对象,则直接将该结果对象返回。
(5)如果二级缓存没有相应的结果对象,则调用底层Executor 对象的query()方法,它会先查询一级缓存,一级缓存未命中时,才会查询数据库。最后还会将得到的结果对象放入集合中保存。
参考
MyBatis技术内幕
深入浅出 MyBatis技术原理与实战