本文内容较多,要耐心看完了,文末为大家准了彩蛋--最权威、最详细的mybatis教程,别忘了购买学习!
目录
MyBatis 如何获取数据库源?MyBatis 如何获取 sql 语句?MyBatis 如何执行 sql 语句?MyBatis 如何实现不同类型数据之间的转换?在过去程序员使用JDBC连接数据库,总会带来诸多不便。MyBatis是一款优秀的持久层框架,可以替代JDBC帮助我们更好地进行开发。要了解MyBatis的实现原理,首先我们要明白MyBatis的大致操作步骤。
数据库源告诉我们连接哪个数据库,获得要执行的SQL语句,再进行操作,这点者缺一不可。接下来要看的就是这三点在底层如何实现。
MyBatis 如何获取数据库源?
使用 Mybatis 第一步肯定是要写好配置文件。官方给出的指导文档告诉我们,XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源。
想要连接数据库,必然要获得数据源信息。既然上述配置文件有数据库源信息,那我们只要进行解析就好了。
由代码可见,我们要的是一个 SqlSessionFactory 实例,SqlSessionFactory 里面就有我们所需的数据库源信息。通过new SqlSessionFactoryBuilder().build(inputStream) 返回SqlSessionFactory 实例,进入 build 方法。
build 方法返回值就是是 SqlSessionFactory,注意到有 return build(parser.parse()),关注点在 parser.parse(),进入 parse 方法。
parse 方法返回 Configuration 。parsed 是一个布尔类型成员变量,默认值是 false,作判断的目的是为了防止多线程情况下该方法被二次调用。
这个方法返回一个 Configuration 类型的实例,Configuration 是 BaseBuilder 类的一个成员变量,Configuration 其实保存了配置文件所有的信息,只是现在还是一张白纸,需要再操作一番。
进入 parseConfiguration 方法。
上一张图的 parse.evalNode 方法将配置文件中 configuration 标签下的内容进行解析,封装到一个对象,这个对象作为参数传入 parseConfiguration 方法中。在 parseConfiguration 方法我们见到了很多熟悉的字样,诸如 properties、typeAliases 之类的配置信息,但我们的目的是要拿到数据库源信息,因此我们把目标放在包裹了数据库源信息的 environments 标签上,进入 environmentsElement 方法。
作为参数的 context 对象与之前一样,封装了 environments 标签中的内容,我们还需要进一步解析 dataSource 标签,关注 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")) 这段代码,进入dataSourceElement 方法。
到这里 context 对象只有 dataSource 里的内容了。发现 type 的值为 POOLED(默认值),props 保存最终的数据库配置信息。DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance() 这一段代码,进入 resolveClass 方法,最终再跳转 resolveAlias 方法中。
注意 value = (Class) typeAliases.get(key),typeAliases 实际上是一个 HashMap,将 POOLED 作为 key 得到了保存的对应的 Class 类型。回到 dataSourceElement 方法。
得到返回的 Class 返回值,利用反射 newInstance 创建对应的 DataSourceFactory 对象,set 方法保存 props ,回到 environmentsElement 方法。
继续执行后面的方法,最终数据库源信息封装到一个 Environment 类型的实例,这个实例又通过 set 方法保存到了 configuration 。configuration 已经处理就绪,被 parse 方法返回。回到之前的 build 方法,将 configuration 作为参数传入至另一个重载的 build 方法。
SqlSessionFactory 本身是一个接口,DefaultSqlSessionFactory 则是实现了 SqlSessionFactory 的实现类,保存好 configuration 之后返回,就得到了我们开头需要的 SqlSessionFactory 实例。
MyBatis 如何获取 sql 语句?
与获取数据库源类似,只要解析Mapper配置文件中的对应标签,就可以获得对应的sql语句。之前我们讲过,SqlSessionFactory中的configuration属性保存数据库源信息,事实上这个configuration属性将整个配置文件的信息都给封装成一个类来保存了。解析的前半部分与之前一样,分歧点在之前提到的parseConfiguration方法,其中在environmentsElement方法下面还有一个mapperElement方法。
配置文件中mappers标签加载mapper文件的方式共有四种:resource、url、class、package。代码中的if-else语句块分别判断四种不同的加载方式,可见package的优先级最高。parent是配置文件中mappers标签中的信息,通过外层的循环一个一个读取多个Mapper文件。这里使用的方式是resource,所以会执行光标所在行的代码块,进入mapperParser.parse()方法。
我们要的是 mapper 标签的内容,因此我们关注 configurationElement(parser.evalNode("/mapper")) 这一句,进入 configurationElement 方法。
context 就是我们解析整个 Mapper 文件 mapper 标签中的内容,既然现在得到了内容,那只需再找到对应的标签就能获得sql语句了。注意 buildStatementFromContext(context.evalNodes("select|insert|update|delete")),我们看到了熟悉的 select、insert、update、delete,这些标签里就有我们写 sql 语句。进入 buildStatementFromContext 方法。
list 保存了我们在 Mapper 文件中写的所有含有 sql 语句的标签元素,用一个循环遍历 list 的每一个元素,分别将每一个元素的信息保存到 statementParser 中。进入 parseStatementNode 方法。
这个方法代码内容很多,仅摘出节选,里面定义了很多局部变量,这些变量用来保存sql语句标签(例如)的参数信息(例如缓存useCache)。再把所有参数传到addMappedStatement中。进入addMappedStatement方法。
MappedStatementstatement=statementBuilder.build(),使用build方法得到MappedStatement实例,这个类封装了每一个含有sql语句标签中所有的信息,再是configuration.addMappedStatement(statement),保存到configuration中。
MyBatis 如何执行 sql 语句?
既然有了SqlSessionFactory,我们可以从中获得SqlSession的实例。开启session的语句是SqlSessionsession=sessionFactory.openSession(),进入openSession方法。
最终会执行openSessionFromDataSource方法。在之前environment已经有了数据库源信息,调用configuration.newExecutor方法。
Executor叫做执行器,Mybatis一共有三种执行器,用一个枚举类ExecutorType保存,分别是SIMPLE,REUSE,BATCH,默认就是SIMPLE。if-else语句判断对应的类型,创建不同的执行器。在代码末端处有个if判断语句,如果cacheEnabled为true,则会创建缓存执行器,默认是为true,即默认开启一级缓存。
回到openSessionFromDataSource方法,最终返回一个DefaultSqlSession实例。得到session我们就可以执行sql语句了。SqlSession提供了在数据库执行SQL命令所需的所有方法。你可以通过SqlSession实例来直接执行已映射的SQL语句,以selectOne方法为例,进入该方法后发现,最终会调用到selectList方法。
configuration.getMappedStatement(statement)得到了我们之前保存的MappedStatement对象,再调用executor.query 方法,调用 query 方法之前会执行 wrapCollection 方法,保存 sql 语句中用户传入的参数。进入 query 方法。
boundSql 里面就有我们要执行的 sql 语句,CacheKey 是用来开启缓存的。执行父类 BaseExecutor 中的 createCacheKey 方法,通过 id,offsetid,limited,sql 组成一个唯一的 key,调用下一个 query 方法。
Cache cache = ms.getCache() 是二级缓存,二级缓存为空,直接调用 query 方法。
list = resultHandler == null ? (List) localCache.getObject(key) : null 传入 key 值在本地查询,如果有返回证明 key 已经缓存到本地,直接从本地缓存获取结果。否则 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql),去数据库查询。
localCache.putObject(key, EXECUTION_PLACEHOLDER) 首先将 key 缓存至本地,下一次查询就能找到这个 key 了。进入 doQuery 方法。
stmt = prepareStatement(handler, ms.getStatementLog()),得到一个 Statement。进入 prepareStatement 方法。
我们看到了一个熟悉的 Connection 对象,这个就是原生 JDBC 的实例对象。回到 doQuery 方法,进入 handler.query(stmt, resultHandler) 方法。
statement 强转型为 PreparedStatement 类型,这下我们又得到了 PreparedStatement 的类型实例了,调用 execute 方法,这个方法也是属于原生 JDBC。执行完成后 return resultSetHandler.handleCursorResultSets(ps),进入 handleCursorResultSets 方法。
ResultSetWrapper rsw = getFirstResultSet(stmt),看到 getFirstResultSet 方法中的 ResultSet rs = stmt.getResultSet(),在这里我们得到了 ResultSet 实例对象,最终 return rs != null ? new ResultSetWrapper(rs, configuration) : null,返回最终结果集。
MyBatis 如何实现不同类型数据之间的转换?
进入上一张图中ResultSetWrapper中可以看到,其中包含三个成员变量columnNames、classNames、jdbcTypes,三者都是ArrayList集合。看一下构造方法。
finalResultSetMetaDatametaData=rs.getMetaData(),metaData就是数据库相关的数据,getColumnCount统计有多少个字段,循环加入到columnNames、jdbcTypes、classNames。columnNames保存的就是实体类中的属性名,jdbcTypes保存的是字段在数据库中的数据类型,classNames保存的是字段在Java中的数据类型,比如Java的String与数据库VARCHAR,MyBatis充当一个中介完成转换,真正实现ORM的核心思想。