本文主要讲解JDBC怎么演变到Mybatis的渐变过程,重点讲解了为什么要将JDBC封装成Mybaits这样一个持久层框架。再而论述Mybatis作为一个数据持久层框架本身有待改进之处。
我们先看看我们最熟悉也是最基础的通过JDBC查询数据库数据,一般需要以下七个步骤:
(1) 加载JDBC驱动
(2) 建立并获取数据库连接
(3) 创建 JDBC Statements 对象
(4) 设置SQL语句的传入参数
(5) 执行SQL语句并获得查询结果
(6) 对查询结果进行转换处理并将处理结果返回
(7) 释放相关资源(关闭Connection,关闭Statement,关闭ResultSet)
以下是具体的实现代码:
public static List
上面我们看到了实现JDBC有七个步骤,哪些步骤是可以进一步封装的,减少我们开发的代码量。
第一步优化:连接获取和释放
问题描述:
数据库连接频繁的开启和关闭本身就造成了资源的浪费,影响系统的性能。
解决问题:
数据库连接的获取和关闭我们可以使用数据库连接池来解决资源浪费的问题。通过连接池就可以反复利用已经建立的连接去访问数据库了。减少连接的开启和关闭的时间。
问题描述:
但是现在连接池多种多样,可能存在变化,有可能采用DBCP的连接池,也有可能采用容器本身的JNDI数据库连接池。
解决问题:
我们可以通过DataSource进行隔离解耦,我们统一从DataSource里面获取数据库连接,DataSource具体由DBCP实现还是由容器的JNDI实现都可以,所以我们将DataSource的具体实现通过让用户配置来应对变化。
第二步优化:SQL统一存取
问题描述:
我们使用JDBC进行操作数据库时,SQL语句基本都散落在各个JAVA类中,这样有三个不足之处:
第一,可读性很差,不利于维护以及做性能调优。
第二,改动Java代码需要重新编译、打包部署。
第三,不利于取出SQL在数据库客户端执行(取出后还得删掉中间的Java代码,编写好的SQL语句写好后还得通过+号在Java进行拼凑)。
解决问题:
我们可以考虑不把SQL语句写到Java代码中,那么把SQL语句放到哪里呢?首先需要有一个统一存放的地方,我们可以将这些SQL语句统一集中放到配置文件或者数据库里面(以key-value的格式存放)。然后通过SQL语句的key值去获取对应的SQL语句。
既然我们将SQL语句都统一放在配置文件或者数据库中,那么这里就涉及一个SQL语句的加载问题。
第三步优化:传入参数映射和动态SQL
问题描述:
很多情况下,我们都可以通过在SQL语句中设置占位符来达到使用传入参数的目的,这种方式本身就有一定局限性,它是按照一定顺序传入参数的,要与占位符一一匹配。但是,如果我们传入的参数是不确定的(比如列表查询,根据用户填写的查询条件不同,传入查询的参数也是不同的,有时是一个参数、有时可能是三个参数),那么我们就得在后台代码中自己根据请求的传入参数去拼凑相应的SQL语句,这样的话还是避免不了在Java代码里面写SQL语句的命运。既然我们已经把SQL语句统一存放在配置文件或者数据库中了,怎么做到能够根据前台传入参数的不同,动态生成对应的SQL语句呢?
解决问题:
第一,我们先解决这个动态问题,按照我们正常的程序员思维是,通过if和else这类的判断来进行是最直观的,这个时候我们想到了JSTL中的
假如前面可以判断没有问题,那么假如判断的结果是true,那么就需要输出的标签里面的SQL片段,但是怎么解决在标签里面使用变量名称的问题呢?这里我们需要使用一种有别于SQL的语法来嵌入变量(比如使用#变量名#)。这样,SQL语句经过解析后就可以动态的生成符合上下文的SQL语句。
还有,怎么区分开占位符变量和非占位变量?有时候我们单单使用占位符是满足不了的,占位符只能为查询条件占位,SQL语句其他地方使用不了。这里我们可以使用#变量名#表示占位符变量,使用$变量名$表示非占位符变量。
第四步优化:结果映射和结果缓存
问题描述:
执行SQL语句、获取执行结果、对执行结果进行转换处理、释放相关资源是一整套下来的。假如是执行查询语句,那么执行SQL语句后,返回的是一个ResultSet结果集,这个时候我们就需要将ResultSet对象的数据取出来,不然等到释放资源时就取不到这些结果信息了。我们从前面的优化来看,以及将获取连接、设置传入参数、执行SQL语句、释放资源这些都封装起来了,只剩下结果处理这块还没有进行封装,如果能封装起来,每个数据库操作都不用自己写那么一大堆Java代码,直接调用一个封装的方法就可以搞定了。
解决问题:
我们分析一下,一般对执行结果的有哪些处理,有可能将结果不做任何处理就直接返回,也有可能将结果转换成一个JavaBean对象返回、一个Map返回、一个List返回等等,结果处理可能是多种多样的。从这里看,我们必须告诉SQL处理器两点:第一,需要返回什么类型的对象;第二,需要返回的对象的数据结构怎么跟执行的结果映射,这样才能将具体的值copy到对应的数据结构上。
接下来,我们可以进而考虑对SQL执行结果的缓存来提升性能。缓存数据都是key-value的格式,那么这个key怎么来呢?怎么保证唯一呢?即使同一条SQL语句几次访问的过程中由于传入参数的不同,得到的执行SQL语句也是不同的。那么缓存起来的时候是多对。但是SQL语句和传入参数两部分合起来可以作为数据缓存的key值。
第五步优化:解决重复SQL语句问题
问题描述:
由于我们将所有SQL语句都放到配置文件中,这个时候会遇到一个SQL重复的问题,几个功能的SQL语句其实都差不多,有些可能是SELECT后面那段不同、有些可能是WHERE语句不同。有时候表结构改了,那么我们就需要改多个地方,不利于维护。
解决问题:
当我们的代码程序出现重复代码时怎么办?将重复的代码抽离出来成为独立的一个类,然后在各个需要使用的地方进行引用。对于SQL重复的问题,我们也可以采用这种方式,通过将SQL片段模块化,将重复的SQL片段独立成一个SQL块,然后在各个SQL语句引用重复的SQL块,这样需要修改时只需要修改一处即可。
我们总结一下上面对JDBC的优化和封装:
(1) 使用数据库连接池对连接进行管理
(2) SQL语句统一存放到配置文件
(3) SQL语句变量和传入参数的映射以及动态SQL
(4) 动态SQL语句的处理
(5) 对数据库操作结果的映射和结果缓存
(6) SQL语句的重复
问题描述:
Mybaits所有的数据库操作都是基于SQL语句,导致什么样的数据库操作都要写SQL语句。一个应用系统要写的SQL语句实在太多了。
改进方法:
我们对数据库进行的操作大部分都是对表数据的增删改查,很多都是对单表的数据进行操作,由这点我们可以想到一个问题:单表操作可不可以不写SQL语句,通过JavaBean的默认映射器生成对应的SQL语句,比如:一个类UserInfo对应于USER_INFO表, userId属性对应于USER_ID字段。这样我们就可以通过反射可以获取到对应的表结构了,拼凑成对应的SQL语句显然不是问题。
本文主要讲解Mybatis的整体程序设计,理清楚框架的主要脉络。后面文章我们再详细讲解各个组件。
(1)加载配置并初始化
触发条件:加载配置文件
配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。
(2)接收调用请求
触发条件:调用Mybatis提供的API
传入参数:为SQL的ID和传入参数对象
处理过程:将请求传递给下层的请求处理层进行处理。
(3)处理操作请求
触发条件:API接口层传递请求过来
传入参数:为SQL的ID和传入参数对象
处理过程:
(A)根据SQL的ID查找对应的MappedStatement对象。
(B)根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数。
(C)获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果。
(D)根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理结果。
(E)释放连接资源。
(4)返回处理结果
将最终的处理结果返回。
功能架构讲解:
我们把Mybatis的功能架构分为三层:
(1)API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
(2)数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
(3)基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
框架架构讲解:
(1)加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。
(2)SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。
(3) SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。
(4)结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。
原理分析之三:初始化(配置文件读取和解析)
1. 准备工作
编写测试代码(具体请参考《Mybatis入门示例》),设置断点,以Debug模式运行,具体代码如下:
String resource = "mybatis.cfg.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = ssf.openSession();
2.源码分析
我们此次就对上面的代码进行跟踪和分析,let's go。
首先我们按照顺序先看看第一行和第二行代码,看看它主要完成什么事情:
String resource = "mybatis.cfg.xml";
Reader reader = Resources.getResourceAsReader(resource);
读取Mybaits的主配置配置文件,并返回该文件的输入流,我们知道Mybatis所有的SQL语句都写在XML配置文件里面,所以第一步就需要读取这些XML配置文件,这个不难理解,关键是读取文件后怎么存放。
我们接着看第三行代码(如下),该代码主要是读取配置文件流并将这些配置信息存放到Configuration类中。
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactoryBuilder的build的方法如下:
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
其实是调用该类的另一个build方法来执行的,具体代码如下:
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
我们重点看一下里面两行:
//创建一个配置文件流的解析对象XMLConfigBuilder,其实这里是将环境和配置文件流赋予解析类
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 解析类对配置文件进行解析并将解析的内容存放到Configuration对象中,并返回SqlSessionFactory
return build(parser.parse());
这里的XMLConfigBuilder初始化其实调用的代码如下:
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
XMLConfigBuilder的parse方法执行代码如下:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each MapperConfigParser can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
解析的内容主要是在parseConfiguration方法中,它主要完成的工作是读取配置文件的各个节点,然后将这些数据映射到内存配置对象Configuration中,我们看一下parseConfiguration方法内容:
private void parseConfiguration(XNode root) {
try {
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
propertiesElement(root.evalNode("properties"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
最后我们看一下第四行代码:
SqlSession session = ssf.openSession();
通过调用DefaultSqlSessionFactory的openSession方法返回一个SqlSession实例,我们看一下具体是怎么得到一个SqlSession实例的。首先调用openSessionFromDataSource方法。
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
下面我们看一下openSessionFromDataSource方法的逻辑:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Connection connection = null;
try {
//获取配置信息里面的环境信息,这些环境信息都是包括使用哪种数据库,连接数据库的信息,事务
final Environment environment = configuration.getEnvironment();
//根据环境信息关于数据库的配置获取数据源
final DataSource dataSource = getDataSourceFromEnvironment(environment);
//根据环境信息关于事务的配置获取事务工厂
TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
connection = dataSource.getConnection();
if (level != null) {
//设置连接的事务隔离级别
connection.setTransactionIsolation(level.getLevel());
}
//对connection进行包装,使连接具备日志功能,这里用的是代理。
connection = wrapConnection(connection);
//从事务工厂获取一个事务实例
Transaction tx = transactionFactory.newTransaction(connection, autoCommit);
//从配置信息中获取一个执行器实例
Executor executor = configuration.newExecutor(tx, execType);
//返回SqlSession的一个默认实例
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeConnection(connection);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
传入参数说明:
(1)ExecutorType:执行类型,ExecutorType主要有三种类型:SIMPLE, REUSE, BATCH,默认是SIMPLE,都在枚举类ExecutorType里面。
(2)TransactionIsolationLevel:事务隔离级别,都在枚举类TransactionIsolationLevel中定义。
(3)autoCommit:是否自动提交,主要是事务提交的设置。
DefaultSqlSession是SqlSession的实现类,该类主要提供操作数据库的方法给开发人员使用。
这里总结一下上面的过程,总共由三个步骤:
步骤一:读取Ibatis的主配置文件,并将文件读成文件流形式(InputStream)。
步骤二:从主配置文件流中读取文件的各个节点信息并存放到Configuration对象中。读取mappers节点的引用文件,并将这些文件的各个节点信息存放到Configuration对象。
步骤三:根据Configuration对象的信息获取数据库连接,并设置连接的事务隔离级别等信息,将经过包装数据库连接对象SqlSession接口返回,DefaultSqlSession是SqlSession的实现类,所以这里返回的是DefaultSqlSession,SqlSession接口里面就是对外提供的各种数据库操作。
上回我们讲到Mybatis加载相关的配置文件进行初始化,这回我们讲一下一次SQL查询怎么进行的。
准备工作
Mybatis完成一次SQL查询需要使用的代码如下:
String resource = "mybatis.cfg.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = ssf.openSession();
try {
UserInfo user = (UserInfo) session.selectOne("User.selectUser", "1");
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
} finally {
session.close();
}
本次我们需要进行深入跟踪分析的是:
SqlSession session = ssf.openSession();
UserInfo user = (UserInfo) session.selectOne("User.selectUser", "1");
源码分析
第一步:打开一个会话,我们看看里面具体做了什么事情。
SqlSession session = ssf.openSession();
DefaultSqlSessionFactory的 openSession()方法内容如下:
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
跟进去,我们看一下openSessionFromDataSource方法到底做了啥:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Connection connection = null;
try {
final Environment environment = configuration.getEnvironment();
final DataSource dataSource = getDataSourceFromEnvironment(environment);
TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
connection = dataSource.getConnection();
if (level != null) {
connection.setTransactionIsolation(level.getLevel());
}
connection = wrapConnection(connection);
Transaction tx = transactionFactory.newTransaction(connection, autoCommit);
Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeConnection(connection);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
这里我们分析一下这里所涉及的步骤:
(1)获取前面我们加载配置文件的环境信息,并且获取环境信息中配置的数据源。
(2)通过数据源获取一个连接,对连接进行包装代理(通过JDK的代理来实现日志功能)。
(3)设置连接的事务信息(是否自动提交、事务级别),从配置环境中获取事务工厂,事务工厂获取一个新的事务。
(4)传入事务对象获取一个新的执行器,并传入执行器、配置信息等获取一个执行会话对象。
从上面的代码我们可以得出,一次配置加载只能有且对应一个数据源。对于上述步骤,我们不难理解,我们重点看看新建执行器和DefaultSqlSession。
首先,我们看看newExecutor到底做了什么?
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
上面代码的执行步骤如下:
(1)判断执行器类型,如果配置文件中没有配置执行器类型,则采用默认执行类型ExecutorType.SIMPLE。
(2)根据执行器类型返回不同类型的执行器(执行器有三种,分别是 BatchExecutor、SimpleExecutor和CachingExecutor,后面我们再详细看看)。
(3)跟执行器绑定拦截器插件(这里也是使用代理来实现)。
DefaultSqlSession到底是干什么的呢?
DefaultSqlSession实现了SqlSession接口,里面有各种各样的SQL执行方法,主要用于SQL操作的对外接口,它会的调用执行器来执行实际的SQL语句。
接下来我们看看SQL查询是怎么进行的
UserInfo user = (UserInfo) session.selectOne("User.selectUser", "1");
实际调用的是DefaultSqlSession类的selectOne方法,该方法代码如下:
public Object selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List list = selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
我们再看看selectList方法(实际上是调用该类的另一个selectList方法来实现的):
public List selectList(String statement, Object parameter) {
return selectList(statement, parameter, RowBounds.DEFAULT);
}
public List selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
第二个selectList的执行步骤如下:
(1)根据SQL的ID到配置信息中找对应的MappedStatement,在之前配置被加载初始化的时候我们看到了系统会把配置文件中的SQL块解析并放到一个MappedStatement里面,并将MappedStatement对象放到一个Map里面进行存放,Map的key值是该SQL块的ID。
(2)调用执行器的query方法,传入MappedStatement对象、SQL参数对象、范围对象(此处为空)和结果处理方式。
好了,目前只剩下一个疑问,那就是执行器到底怎么执行SQL的呢?
上面我们知道了,默认情况下是采用SimpleExecutor执行的,我们看看这个类的doQuery方法:
public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler);
stmt = prepareStatement(handler);
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
doQuery方法的内部执行步骤:
(1) 获取配置信息对象。
(2)通过配置对象获取一个新的StatementHandler,该类主要用来处理一次SQL操作。
(3)预处理StatementHandler对象,得到Statement对象。
(4)传入Statement和结果处理对象,通过StatementHandler的query方法来执行SQL,并对执行结果进行处理。
我们看一下newStatementHandler到底做了什么?
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
上面代码的执行步骤:
(1)根据相关的参数获取对应的StatementHandler对象。
(2)为StatementHandler对象绑定拦截器插件。
RoutingStatementHandler类的构造方法RoutingStatementHandler如下:
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
根据 MappedStatement对象的StatementType来创建不同的StatementHandler,这个跟前面执行器的方式类似。StatementType有STATEMENT、PREPARED和CALLABLE三种类型,跟JDBC里面的Statement类型一一对应。
我们看一下prepareStatement方法具体内容:
private Statement prepareStatement(StatementHandler handler) throws SQLException {
Statement stmt;
Connection connection = transaction.getConnection();
//从连接中获取Statement对象
stmt = handler.prepare(connection);
//处理预编译的传入参数
handler.parameterize(stmt);
return stmt;
}