一、Mybatis架构:
1、接口层
接口层主要定义的是与数据库进行交互的方式。在Mybatis中,交互分为两种方式。
(1) Mybatis提供的API
使用Mybatis提供的API进行操作,通过获取SqlSession对象,然后根据Statement Id 和参数来操作数据库。
String statement = "com.viewscenes.netsupervisor.dao.UserMapper.getUserList";
List<User> result = sqlsession.selectList(statement);
(2) 使用Mapper接口
事实上这个才是经常使用的方式,面向接口编程嘛。每一个Mapper接口中的方法对应着mapper.xml文件中的一个select/insert/update/delete节点。节点中的ID就是接口中的方法名,在使用的时候直接调用接口方法即可。不过,值得注意的是,它最终执行的还是sqlSession.select()、sqlSession.delete()。
2、数据处理层
这是Mybatis的核心。它负责参数映射和动态SQL生成,生成之后Mybatis执行SQL语句,并将返回的结果映射成自定义的类型。关于参数映射和结果集转换,主要是靠typeHandlers。为便于理解,我们大概来看几个类型处理器。
类型处理器 | Java类型 | JDBC类型 |
StringTypeHandler | java.lang.String | CHAR, VARCHAR |
DateTypeHandler | java.util.Date | TIMESTAMP |
BooleanTypeHandler | java.lang.Boolean, boolean | 数据库兼容的 BOOLEAN |
IntegerTypeHandler | java.lang.Integer, int | 数据库兼容的 NUMERIC 或 INTEGER |
3、框架支撑层
-
事务管理
对于ORM框架而言,事务管理是必不可少的一部分。不过,一般情况下,Mybatis都是和Spring搭配使用的,更多的是用Spring来接管事务管理。
-
连接池
我们不能每次在执行SQL的时候才去创建数据库的连接。因为创建连接是一个相对比较耗时的操作,通常做法是用一个列表保存提前创建好的N个连接,用到的时候去拿,用完再还回去。关于数据库连接池,业界有很多开源实现。比如C3P0、DBCP、Tomcat Jdbc Pool、BoneCP、Druid等。
-
缓存(一般不直接使用Mybatis的缓存,而是使用第三方的缓存)
为了提高数据利用率和减小服务器和数据库的压力,Mybatis 会对于一些查询提供会话级别的数据缓存,会将对某一次查询,放置到SqlSession 中,在允许的时间间隔内,对于完全相同的查询,MyBatis 会直接将缓存结果返回给用户,而不用再到数据库中查找。
一级缓存是SqlSession级别的缓存,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率,Mybtais默认开启一级缓存。
二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据库得到数据会存在二级缓存区域,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。要开启二级缓存,需要在你的 SQL 映射文件中添加一行:<cache/>。它会将所有的select语句缓存,在执行insert,update 和 delete 语句时会刷新缓存,缓存根据LRU算法来回收。
4、引导层
引导层是配置和启动MyBatis 配置信息的方式。MyBatis 提供两种方式来引导MyBatis :基于XML配置文件的方式和基于Java API 的方式。
SQL配置方式
大部分时候我们都是通过XML方式来配置SQL,不过Mybatis也支持通过注解来配置,就像下面这样( 不过,不推荐这种方式)。
@Select({"<script>", "select * from user"
"</script>"})
List<ConsultContent> getUserList();
主要构件
-
SqlSession
作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。
-
Executor
MyBatis执行器,是MyBatis 调度的核心,负责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对象之中。
二、XML的解析
1.初始化
在使用时,我们以 SqlSessionFactoryBuilder 去创建 SqlSessionFactory:
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
那么咱们先看看源码是怎么实现的:
首先,SqlSessionFactory是一个接口,它里面其实就两个方法:
-
openSession
-
getConfiguration。
package org.apache.ibatis.session;
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
然后使用SqlSessionFactoryBuilder构建出一个具体的实现类DefaultSqlSessionFactory。
这个DefaultSqlSessionFactory类里面有一个Configuration对象,保存了mapper映射文件、SQL参数、返回值类型、缓存等属性。
再继续看SqlSessionFactoryBuilder类的build()方法,其实内部调用了XMLConfigBuilder这个类的parse()方法解析我们的配置文件构建出一个Configuration对象,然后传入了DefaultSqlSessionFactory的构造函数得到一个DefaultSqlSessionFactory对象。那就继续看XMLConfigBuilder,XMLConfigBuilder类继承自BaseBuilder,是XML配置构建器。
BaseBuilder中有是三个属性: Configuration, TypeAliasRegistry和TypeHandlerRegistry:
public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
}
XMLConfigBuilder中的解析流程主要就是parseConfiguration()这个方法:
/**
* mybatis 配置文件解析
*/
public class XMLConfigBuilder extends BaseBuilder {
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
此方法就是解析configuration节点下的子节点。由此也可看出,我们在configuration下面能配置的节点为以下10个节点
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
……
}
通过以上源码,我们就能看出,在mybatis的配置文件中:
-
configuration节点为根节点。
-
在configuration节点之下,我们可以配置10个子节点, 分别为:properties、typeAliases、plugins、objectFactory、objectWrapperFactory、settings、environments、databaseIdProvider、typeHandlers、mappers。
2.解析配置文件中的properties
上次我们说过mybatis 是通过XMLConfigBuilder这个类在解析mybatis配置文件的,那么本次就接着看看XMLConfigBuilder对于properties和environments的解析:
properties的使用方法:
<configuration>
<!-- 方法一: 从外部指定properties配置文件, 除了使用resource属性指定外,还可通过url属性指定url
<properties resource="dbConfig.properties"></properties>
-->
<!-- 方法二: 直接配置为xml -->
<properties>
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test1"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</properties>
首先看XMLConfigBuilder中的propertiesElement()方法
//下面就看看解析properties的具体方法
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//将子节点的 name 以及value属性set进properties对象
//这儿可以注意一下顺序,xml配置优先, 外部指定properties配置其次
Properties defaults = context.getChildrenAsProperties();
//获取properties节点上 resource属性的值
String resource = context.getStringAttribute("resource");
//获取properties节点上 url属性的值, resource和url不能同时配置
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
//把解析出的properties文件set进Properties对象
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
//将configuration对象中已配置的Properties属性与刚刚解析的融合
//configuration这个对象会装载所解析mybatis配置文件的所有节点元素,以后也会频频提到这个对象
//既然configuration对象用有一系列的get/set方法, 那是否就标志着我们可以使用java代码直接配置?
//答案是肯定的, 不过使用配置文件进行配置,优势不言而喻
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
//把装有解析配置propertis对象set进解析器, 因为后面可能会用到
parser.setVariables(defaults);
//set进configuration对象
configuration.setVariables(defaults);
}
}
解析完成之后直接调用Configuration的setVariables()方法保存到Configuration的 variables 中:
public void setVariables(Properties variables) {
this.variables = variables;
}
3.解析配置文件中的envirements
envirements元素节点的使用方法:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--
如果上面没有指定数据库配置的properties文件,那么此处可以这样直接配置
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test1"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
-->
<!-- 上面指定了数据库配置文件, 配置文件里面也是对应的这四个属性 -->
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
<!-- 我再指定一个environment -->
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<!-- 与上面的url不一样 -->
<property name="url" value="jdbc:mysql://localhost:3306/demo"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
environments元素节点可以配置多个environment子节点, 怎么理解呢?假如我们系统的开发环境和正式环境所用的数据库不一样(这是肯定的), 那么可以设置两个environment, 两个id分别对应开发环境(dev)和正式环境(final),那么通过配置environments的default属性就能选择对应的environment了, 例如,我将environments的deault属性的值配置为dev, 那么就会选择dev的environment。
具体的解析方法是在XMLConfigBuilder中的environmentsElement()方法中:
//下面再看看解析enviroments元素节点的方法
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
//解析environments节点的default属性的值
//例如: <environments default="development">
environment = context.getStringAttribute("default");
}
//递归解析environments子节点
for (XNode child : context.getChildren()) {
//<environment id="development">, 只有enviroment节点有id属性,那么这个属性有何作用?
//environments 节点下可以拥有多个 environment子节点
//类似于这样: <environments default="development"><environment id="development">...</environment><environment id="test">...</environments>
//意思就是我们可以对应多个环境,比如开发环境,测试环境等, 由environments的default属性去选择对应的enviroment
String id = child.getStringAttribute("id");
//isSpecial就是根据由environments的default属性去选择对应的enviroment
if (isSpecifiedEnvironment(id)) {
//事务, mybatis有两种:JDBC 和 MANAGED, 配置为JDBC则直接使用JDBC的事务,配置为MANAGED则是将事务托管给容器,
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//enviroment节点下面就是dataSource节点了,解析dataSource节点(下面会贴出解析dataSource的具体方法)
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
//老规矩,会将dataSource设置进configuration对象
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
//下面看看dataSource的解析方法
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
//dataSource的连接池
String type = context.getStringAttribute("type");
//子节点 name, value属性set进一个properties对象
Properties props = context.getChildrenAsProperties();
//创建dataSourceFactory
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
还有一个问题, 上面我们看到,在配置dataSource的时候使用了 ${driver} 这种表达式, 这种形式是怎么解析的?其实,是通过PropertyParser这个类解析。在XNode类的parseAttributes()方法中调用了PropertyParser的parse()方法:
4.解析配置文件中的typeAliases
typeAliases节点主要用来设置别名,其实这是挺好用的一个功能, 通过配置别名,我们不用再指定完整的包名,并且还能取别名。例如: 我们在使用 com.demo.entity. UserEntity 的时候,我们可以直接配置一个别名user, 这样以后在配置文件中要使用到com.demo.entity. UserEntity的时候,直接使用User即可。就以上例为例,我们来实现一下,看看typeAliases的配置方法:
<configuration>
<typeAliases>
<!--
通过package, 可以直接指定package的名字, mybatis会自动扫描你指定包下面的javabean,
并且默认设置一个别名,默认的名字为: javabean 的首字母小写的非限定类名来作为它的别名。
也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user)
<package name="com.dy.entity"/>
-->
<typeAlias alias="UserEntity" type="com.dy.entity.User"/>
</typeAliases>
......
</configuration>
先从XMLConfigBuilder的typeAliasesElement()方法开始:
/**
* 解析typeAliases节点
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果子节点是package, 那么就获取package节点的name属性, mybatis会扫描指定的package
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
//TypeAliasRegistry 负责管理别名, 这儿就是通过TypeAliasRegistry 进行别名注册, 下面就会看看TypeAliasRegistry源码
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
//如果子节点是typeAlias节点,那么就获取alias属性和type的属性值
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
这有个很重要的类TypeAliasRegistry,TypeAliasRegistry内部维护了一个TYPE_ALIASES属性,它是一个HashMap,所以别名其实就是通过一个HashMap来实现, key为别名, value就是别名对应的类型(class对象),里面已经默认添加了很多的类型别名。。
解析完成然后调用TypeAliasRegistry类的registerAlias()方法注册到容器中。
public class TypeAliasRegistry {
private final Map<String, Class<?>> typeAliases = new HashMap<>();
public void registerAlias(Class<?> type) {
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
String key = alias.toLowerCase(Locale.ENGLISH);
TYPE_ALIASES.put(key, type);
}
......
}
在使用时,除了在xml中配置aliases,还可以使用@Alias注解来设置别名的名称。
5.解析配置中的typeHanlder
5.1 typeHanlder的解析
它是一个类型的处理器。在数据库查询出结果后,应该转换成Java中的什么类型?由它来决定。如果Mybatis里面没有你想要的,就可以在这里自定义一个处理器。例如:
现在,先来看下它的默认处理器。
看一下IntegerTypeHanlder:
/**
* @author Clinton Begin
*/
/**
* Integer类型处理器
* 调用PreparedStatement.setInt, ResultSet.getInt, CallableStatement.getInt
*
*/
public class IntegerTypeHandler extends BaseTypeHandler<Integer> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter);
}
@Override
public Integer getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return rs.getInt(columnName);
}
@Override
public Integer getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
return rs.getInt(columnIndex);
}
@Override
public Integer getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return cs.getInt(columnIndex);
}
}
看一下XMLConfigBuilder中的typeHandlerElement()方法:
/**
* 解析typeHandlers节点
*/
private void typeHandlerElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//子节点为package时,获取其name属性的值,然后自动扫描package下的自定义typeHandler
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
//子节点为typeHandler时, 可以指定javaType属性, 也可以指定jdbcType, 也可两者都指定
//javaType 是指定java类型
//jdbcType 是指定jdbc类型(数据库类型: 如varchar)
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
//handler就是我们配置的typeHandler
String handlerTypeName = child.getStringAttribute("handler");
//resolveClass方法就是我们上篇文章所讲的TypeAliasRegistry里面处理别名的方法
Class<?> javaTypeClass = resolveClass(javaTypeName);
//JdbcType是一个枚举类型,resolveJdbcType方法是在获取枚举类型的值
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
//注册typeHandler, typeHandler通过TypeHandlerRegistry这个类管理
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
解析完成之后都是注册到XMLConfigBuilder的typeHandlerRegistry属性中。TypeHandlerRegistry这个类和上面的TypeAliasRegistry有点类似,内部也是维护了一个HashMap,key为Java类型,value为自定义的Handler类型。
5.2 自定义TypeHandler
(1) 案例分析
在日常开发中,我们肯定有对日期类型的操作。比如订单时间、付款时间等,通常这一类数据在数据库以datetime类型保存。如果需要在页面上展示此值,在Java中以什么类型接收它呢?
在不执行任何二次操作的情况下:
用java.util.Date接收,在页面展示的就是Tue Oct 16 16:05:13 CST 2018。
用java.lang.String接收,在页面展示的就是2018-10-16 16:10:47.0。
显然,我们不能显示第一种。第二种似乎可行,但大部分情况下不能出现毫秒数。当然了,不管哪种方式,在显示的时候format一下当然是可行的。有没有更好的方式呢?那就是typeHanlders
(2) typeHandlers
无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。
在数据库中,datetime和timestamp类型含义是一样的,不过timestamp存储空间小, 所以它表示的时间范围也更小。
下面来看几个Mybatis默认的时间类型处理器。
JDBC 类型 | Java 类型 | 类型处理器 |
DATE | java.util.Date | DateOnlyTypeHandler |
DATE | java.sql.Date | SqlDateTypeHandler |
DATE | java.time.LocalDate | LocalDateTypeHandler |
DATE | java.time.LocalTime | LocalTimeTypeHandler |
TIMESTAMP | java.util.Date | DateTypeHandler |
TIMESTAMP | java.time.Instant | InstantTypeHandler |
TIMESTAMP | java.time.LocalDateTime | LocalDateTimeTypeHandler |
TIMESTAMP | java.sql.Timestamp | SqlTimestampTypeHandler |
它是什么意思呢?如果数据库字段类型为JDBC 类型,同时Java字段的类型为Java 类型,那么就调用类型处理器类型处理器。
(3) 自定义处理器
基于上面这个逻辑,我们可以增加一种处理器来处理我们开头所描述的问题。我们可以在Java中,以String类型接收数据库的DateTime类型数据。因为现在的接口以restful风格居多,用String类型方便传输。
最后的毫秒数通过自定义的处理器统一截取去除即可。
JDBC 类型 | Java 类型 | 类型处理器 |
TIMESTAMP | java.lang.String | CustomTypeHandler |
(1)xml中配置自定义类型处理器
<property name="typeHandlers">
<array>
<bean class="com.viewscenes.netsupervisor.util.CustomTypeHandler"></bean>
</array>
</property>
(2)在对应的CustomTypeHandler类上加上注解
@MappedJdbcTypes注解表示JDBC的类型,@MappedTypes表示Java属性的类型。
@MappedJdbcTypes({ JdbcType.TIMESTAMP })@MappedTypes({ String.class })
public class CustomTypeHandler extends BaseTypeHandler<String>{
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return substring(rs.getString(columnName));
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
private String substring(String value) {
if (!"".endsWith(value) && value != null) {
return value.substring(0, value.length() - 2);
}
return value;
}
}
通过以上方式,我们就可以放心的在Java中以String接收数据库的时间类型数据了。
6.objectFactory、plugins简介与配置解析
6.1 objectFactory是干什么的? 需要配置吗?
MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。默认情况下,我们不需要配置,mybatis会调用默认实现的objectFactory。 除非我们要自定义ObjectFactory的实现, 那么我们才需要去手动配置。
那么怎么自定义实现ObjectFactory? 怎么配置呢?
自定义ObjectFactory只需要去继承DefaultObjectFactory(是ObjectFactory接口的实现类),并重写其方法即可。具体的,本处不多说,后面再具体讲解。
写好了ObjectFactory, 仅需做如下配置:
<configuration>
......
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>
......
</configuration
6.2 plugins
可以配置一个或多个插件。插件功能很强大,在执行SQL之前,在返回结果之后,在插入数据时...都可以让你有机会插手数据的处理(拦截器)。这个部分最常用的是分页,在后续章节,笔者将通过分页和数据同步的实例单独讲解。
<property name="plugins">
<array>
<bean class="com.viewscenes.netsupervisor.interceptor.xxxInterceptor"></bean>
<bean class="com.viewscenes.netsupervisor.interceptor.xxxInterceptor"></bean>
</array>
</property>
XMLConfigBuilder中的objectFactoryElement()方法和pluginElement()方法:
/**
* objectFactory 节点解析
*/
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
//读取type属性的值, 接下来进行实例化ObjectFactory, 并set进 configuration
//到此,简单讲一下configuration这个对象,其实它里面主要保存的都是mybatis的配置
String type = context.getStringAttribute("type");
//读取propertie的值, 根据需要可以配置, mybatis默认实现的objectFactory没有使用properties
Properties properties = context.getChildrenAsProperties();
ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
factory.setProperties(properties);
configuration.setObjectFactory(factory);
}
}
/**
* plugins 节点解析
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
//由此可见,我们在定义一个interceptor的时候,需要去实现Interceptor, 这儿先不具体讲,以后会详细讲解
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
7.解析配置中的mappers
mapper映射文件的配置, 这是mybatis的核心之一,一定要学好。在mapper文件中,以mapper作为根节点,其下面可以配置的元素节点有: select, insert, update, delete, cache, cache-ref, resultMap, sql 。
7.1 mapper中的配置
看看 insert, update, delete 怎么配置, 能配置哪些元素吧:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<!-- mapper 为根元素节点, 一个namespace对应一个dao -->
<mapper namespace="com.dy.dao.UserDao">
<insert
<!-- 1. id (必须配置)
id是命名空间中的唯一标识符,可被用来代表这条语句。
一个命名空间(namespace) 对应一个dao接口,
这个id也应该对应dao里面的某个方法(相当于方法的实现),因此id 应该与方法名一致 -->
id="insertUser"
<!-- 2. parameterType (可选配置, 默认为mybatis自动选择处理)
将要传入语句的参数的完全限定类名或别名, 如果不配置,mybatis会通过ParameterHandler 根据参数类型默认选择合适的typeHandler进行处理
parameterType 主要指定参数类型,可以是int, short, long, string等类型,也可以是复杂类型(如对象) -->
parameterType="com.demo.User"
<!-- 3. flushCache (可选配置,默认配置为true)
将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true(对应插入、更新和删除语句) -->
flushCache="true"
<!-- 4. statementType (可选配置,默认配置为PREPARED)
STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 -->
statementType="PREPARED"
<!-- 5. keyProperty (可选配置, 默认为unset)
(仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认:unset。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 -->
keyProperty=""
<!-- 6. keyColumn (可选配置)
(仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 -->
keyColumn=""
<!-- 7. useGeneratedKeys (可选配置, 默认为false)
(仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。 -->
useGeneratedKeys="false"
<!-- 8. timeout (可选配置, 默认为unset, 依赖驱动)
这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。 -->
timeout="20">
<update
id="updateUser"
parameterType="com.demo.User"
flushCache="true"
statementType="PREPARED"
timeout="20">
<delete
id="deleteUser"
parameterType="com.demo.User"
flushCache="true"
statementType="PREPARED"
timeout="20">
</mapper>
此外,如果我们在使用mysql的时候,想在数据插入后返回插入的id, 我们也可以使用 selectKey 这个元素:
<!-- 对应userDao中的insertUser方法, -->
<insert id="insertUser" parameterType="com.dy.entity.User">
<!-- oracle等不支持id自增长的,可根据其id生成策略,先获取id
<selectKey resultType="int" order="BEFORE" keyProperty="id">
select seq_user_id.nextval as id from dual
</selectKey>
-->
<!-- mysql插入数据后,获取id -->
<selectKey keyProperty="id" resultType="int" order="AFTER" >
SELECT LAST_INSERT_ID() as id
</selectKey>
insert into user(id, name, password, age, deleteFlag)
values(#{id}, #{name}, #{password}, #{age}, #{deleteFlag})
</insert>
selectKey的具体用法:
<selectKey
<!-- selectKey 语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 -->
keyProperty="id"
<!-- 结果的类型。MyBatis 通常可以推算出来,但是为了更加确定写上也不会有什么问题。MyBatis 允许任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列,则可以使用一个包含期望属性的 Object 或一个 Map。 -->
resultType="int"
<!-- 这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它会首先选择主键,设置 keyProperty 然后执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 元素 - 这和像 Oracle 的数据库相似,在插入语句内部可能有嵌入索引调用。 -->
order="BEFORE"
<!-- 与前面相同,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 语句的映射类型,分别代表 PreparedStatement 和 CallableStatement 类型。 -->
statementType="PREPARED">
还有一个resultMap 这个东西, mybatis的resultMap功能可谓十分强大,能够处理复杂的关系映射, 那么resultMap 该怎么配置呢? resultMap的配置:
<!--
1.type 对应类型,可以是javabean, 也可以是其它
2.id 必须唯一, 用于标示这个resultMap的唯一性,在使用resultMap的时候,就是通过id指定
-->
<resultMap type="" id="">
<!-- id, 唯一性,注意啦,这个id用于标示这个javabean对象的唯一性, 不一定会是数据库的主键(不要把它理解为数据库对应表的主键)
property属性对应javabean的属性名,column对应数据库表的列名
(这样,当javabean的属性与数据库对应表的列名不一致的时候,就能通过指定这个保持正常映射了)
-->
<id property="" column=""/>
<!-- result与id相比, 对应普通属性 -->
<result property="" column=""/>
<!--
constructor对应javabean中的构造方法
-->
<constructor>
<!-- idArg 对应构造方法中的id参数 -->
<idArg column=""/>
<!-- arg 对应构造方法中的普通参数 -->
<arg column=""/>
</constructor>
<!--
collection,对应javabean中容器类型, 是实现一对多的关键
property 为javabean中容器对应字段名
column 为体现在数据库中列名
ofType 就是指定javabean中容器指定的类型
-->
<collection property="" column="" ofType=""></collection>
<!--
association 为关联关系,是实现N对一的关键。
property 为javabean中容器对应字段名
column 为体现在数据库中列名
javaType 指定关联的类型
-->
<association property="" column="" javaType=""></association>
</resultMap>
其实就是查询结果为一个列表。
7.2 mapper的解析
首先调用XMLConfigBuilder的mapperElement()方法:
这里又涉及到另一个类XMLMapperBuilder,它也是继承自BaseBuilder。
通过解析里面的select/insert/update/delete节点,每一个节点生成一个MappedStatement对象。最后注册到Configuration对象的 。key为mapper的namespace+节点id。
在XMLMapperBuilder类的configurationElement()方法中:
public class XMLMapperBuilder extends BaseBuilder {
private void configurationElement(XNode context) {
//命名空间 即mapper接口的路径
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//设置当前mapper文件的命名空间
builderAssistant.setCurrentNamespace(namespace);
//引用缓存
cacheRefElement(context.evalNode("cache-ref"));
//是否开启二级缓存
cacheElement(context.evalNode("cache"));
//参数
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//返回值
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql节点
sqlElement(context.evalNodes("/mapper/sql"));
//SQL语句解析
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}}
7.3 resultMap、parameterMap的解析
它们表示的是查询结果集中的列与Java对象中属性的对应关系。其实,只有在数据库字段与JavaBean不匹配的情况下才用到,通常情况下推荐使用resultType/parameterType,也就是直接利用实体类即可。这种方式很简便,同时遵循约定大于配置,代码出错的可能较小。
XMLMapperBuilder中有个mapperBuilderAssistants属性,在解析mapper的过程中,会调用MapperBuilderAssistant类的addParameterMap方法,然后将resultMap或者parameterMap都注册到configuration中:
public void addParameterMap(ParameterMap pm) {
parameterMaps.put(pm.getId(), pm);
}
public void addResultMap(ResultMap rm) {
resultMaps.put(rm.getId(), rm);
}
三、mapper中SQL的使用和解析
1.例子:
(1) if
<select id="findUserById" resultType="user">
select * from user where
<if test="id != null">
id=#{id}
</if>
and deleteFlag=0;
</select>
上面例子: 如果传入的id 不为空, 那么才会SQL才拼接id = #{id}。
但是如果传入的id为null, 那么你这最终的SQL语句不就成了 select * from user where and deleteFlag=0, 这语句有问题!。这时候,mybatis的 where 标签就该隆重登场啦,改造一下:
(2) where
<select id="findUserById" resultType="user">
select * from user <where>
<if test="id != null">
id=#{id}
</if>
and deleteFlag=0;
</where>
</select>
有些人就要问了: “你这都是些什么玩意儿! 跟上面的相比, 不就是多了个where标签嘛! 那这个还会不会出现 select * from user where and deleteFlag=0 ?”
的确,从表面上来看,就是多了个where标签而已, 不过实质上, mybatis是对where做了处理,当它遇到AND或者OR这些,它可以自动去除and或者or前后的语句。
(3) trim
其实我们可以手动通过 trim 标签去自定义这种处理规则。例如:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
它的意思就是: 当WHERE后紧随AND或则OR的时候,就去除AND或者OR。 除了WHERE以外, 其实还有一个比较经典的实现,那就是SET。
(4) set
<update id="updateUser" parameterType="com.dy.entity.User">
update user set
<if test="name != null">
name = #{name},
</if>
<if test="password != null">
password = #{password},
</if>
<if test="age != null">
age = #{age}
</if>
<where>
<if test="id != null">
id = #{id}
</if>
and deleteFlag = 0;
</where>
</update>
问题又来了: “如果我只有name不为null, 那么这SQL不就成了 update set name = #{name}, where ........ ? 你那name后面那逗号会导致出错啊!”
是的,这时候,就可以用mybatis为我们提供的set 标签了。下面是通过set标签改造后:
<update id="updateUser" parameterType="com.dy.entity.User">
update user <set>
<if test="name != null">name = #{name},</if>
<if test="password != null">password = #{password},</if>
<if test="age != null">age = #{age},</if>
</set>
<where>
<if test="id != null">
id = #{id}
</if>
and deleteFlag = 0;
</where>
</update>
这个用trim 可表示为:
<trim prefix="SET" suffixOverrides=",">
...
</trim>
总结一下,WHERE是使用的 prefixOverrides(前缀), SET是使用的 suffixOverrides (后缀), 看明白了吧!
(5) foreach
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
将一个 List 实例或者数组作为参数对象传给 MyBatis,当这么做的时候,MyBatis 会自动将它包装在一个 Map 中并以名称为键。List 实例将会以“list”作为键,而数组实例的键将是“array”。同样, 当循环的对象为map的时候,index其实就是map的key。
(6) choose+when
Java中有switch, mybatis有choose。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
以上例子中: 当title和author都不为null的时候, 那么选择二选一(前者优先), 如果都为null, 那么就选择 otherwise中的, 如果tilte和author只有一个不为null, 那么就选择不为null的那个。
2.动态SQL的解析
-
SQL标签
SQL标签可将重复的sql提取出来,使用时用include引用即可,最终达到sql重用的目的。它的解析很简单,就是把内容放入sqlFragments容器。id为命名空间+节点ID
-
动态SQL
动态SQL的解析是Mybatis的核心所在。之所以是动态SQL,源自它不同的动态标签,比如Choose、ForEach、If、Set等,而Mybatis把它们都封装成不同的类对象,它们共同的接口是SqlNode。
每一种标签又对应一种处理器。
一个动态SQL会分为不同的子节点,我们以一个UPDATE语句为例,尝试跟踪下它的解析过程。比如下面的UPDATE节点会分为三个子节点。两个静态节点和一个SET动态节点,而SET节点又分为两个IF动态节点。
<update id="updateUser" parameterType="user">
update user
<set>
<if test="username!=null">
username = #{username},
</if>
<if test="password!=null">
password = #{password},
</if>
</set>
where uid = #{uid}
</update>
[
[#text: update user ],
[set: null],
[#text: where uid = #{uid}]
]
在解析mapper的过程中,获取到每个mapper之后,调用XMLStatementBuilder的parseStatementNode()方法:
然后这个方法内部会调用XMLLanguageDriver的createSqlSource()方法:
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
createSqlSource()方法的内容:
所以具体的解析过程是在XMLScriptBuilder类中:
public class XMLScriptBuilder extends BaseBuilder {
//参数node即为当前UDATE节点的内容
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
//children分为3个子节点
//2个静态节点(update user和where id=#{id})
//1个动态节点set
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
//如果是静态节点,将内容封装成StaticTextSqlNode对象
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
}
//动态节点
else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
//获取节点名称 比如SET/IF
String nodeName = child.getNode().getNodeName();
//获取节点标签对应的处理类 比如SetHandler
NodeHandler handler = nodeHandlerMap.get(nodeName);
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
}
第一个是静态节点update user。根据上面的源码,它将被封装成StaticTextSqlNode对象,加入contents集合。
第二个是动态节点SET,他将调用到SetHandler.handleNode()。在第二次回调到parseDynamicTags方法时,这时候的参数为SET节点里的2个IF子节点。同样,它们会将当做动态节点解析,调用到IfHandler.handleNode()。
就这样,递归的调用parseDynamicTags方法,直到传进来的参数Node为一个静态节点,返回StaticTextSqlNode对象,并加入集合中。
第三个是静态节点where id=#{id},封装成StaticTextSqlNode对象,加入contents集合。
最后,contents集合就是UPDATE节点对应的各种sqlNode。
对照一下原来的mapper.xml:
如果是动态SQL,返回DynamicSqlSource对象:
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
3.静态SQL的解析
静态SQL,就是不带上面那些动态标签的节点。它比较简单,可能就是一个单独的select,update,insert或者delete的sql语句,最后就是将SQL内容封装到StaticTextSqlNode对象。MixedSqlNode对象里面有个List,封装的就是StaticTextSqlNode对象,而StaticTextSqlNode对象只有一个属性text,即SQL内容。
解析SQL内容:
protected MixedSqlNode parseDynamicTags(XNode node) {
//SQL内容
String data = child.getStringBody("");
//生成TextSqlNode判断是否为动态SQL
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
return new MixedSqlNode(contents);
}
StaticTextSqlNode类:
如果是静态SQL,将SQL语句中的#{}转为?,返回StaticSqlSource对象 :
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
可以看到DynamicSqlSource和StaticSqlSource都有一个getBoundSql()方法返回一个BoundSql对象,这个BoundSql里面包含了一个完整的可执行的sql语句。
-
MappedStatement对象
mapper文件中的每一个SELECT/INSERT/UPDATE/DELETE节点对应一个MappedStatement对象。
在XMLStatementBuilder的parseStatementNode()方法中,解析到sqlSource,resultMap,parameterMap等内容后,调用builderAssistant的addMappedStatement()方法:
生成一个MappedStatement对象,并注册到configuration中:
public MappedStatement addMappedStatement() {
//全限定类名+方法名
id = applyCurrentNamespace(id, false);
//是否为查询语句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//配置各种属性
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
//参数类型
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
//将MappedStatement对象注册到configuration
//注册其实就是往Map中添加。mappedStatements.put(ms.getId(), ms);
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
四、Annotation的支持
1、注解式的SQL定义
除了在mapper文件中配置SQL,Mybatis还支持注解方式的SQL。通过@Select,标注在Mapper接口的方法上。
public interface UserMapper {
@Select("select * from user ")
List<User> AnnotationGetUserList();
}
或者你想要的是动态SQL,那么就加上<script>:
public interface UserMapper {
@Select("select * from user ")
List<User> AnnotationGetUserList();
@Select("<script>"
+ "select * from user "
+ "<if test='id!=null'>"
+ "where id=#{id}"
+ "</if>"
+ "</script>")
List<User> AnnotationGetUserById(@Param("id")String id);
}
以上这两种方式都不常用,如果你真的不想用mapper.xml文件来定义SQL,那么以下方式可能适合你。你可以通过@SelectProvider来声明一个类的方法,此方法负责返回一个SQL的字符串:
public interface UserMapper {
@SelectProvider(type=SqlProvider.class,method="getUserById")
List<User> AnnotationProviderGetUserById(String id);
}
types指定了类的Class,method就是类的方法。其实这种方式也很不错,动态SQL的生成不仅仅依靠Mybatis的动态标签,在程序中可以随便搞:
public class SqlProvider {
public String getUserById(String id) {
String sql = "select * from user ";
if (id!=null) {
sql += " where id="+id;
}
return sql;
}
注解先通过getSqlSourceFromAnnotations()方法拿到注解上的值解析成SqlSource对象,然后也是生成MappedStatement对象,然后调用configuration.addMappedStatement(statement)方法注册到configuration中,这个流程是与XML的方式一样,不会变的。
五、SQL语句的执行过程
1. SqlSessionFactory 与 SqlSession.
通过前面的章节对于mybatis 的介绍及使用,大家都能体会到SqlSession的重要性了吧, 没错,从表面上来看,咱们都是通过SqlSession去执行sql语句(注意:是从表面看,实际的待会儿就会讲)。那么咱们就先看看是怎么获取SqlSession的吧:
(1)首先,SqlSessionFactoryBuilder去读取mybatis的配置文件,然后build一个DefaultSqlSessionFactory。
(2)当我们获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象。源码如下:
/**
* 通常一系列openSession方法最终都会调用本方法
* @param execType
* @param level
* @param autoCommit
* @return
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装
final Executor executor = configuration.newExecutor(tx, execType);
//关键看这儿,创建了一个DefaultSqlSession对象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
通过以上步骤,咱们已经得到SqlSession对象了。接下来,当然是执行sql语句咯。
SqlSession咱们也拿到了,咱们可以调用SqlSession中一系列的select..., insert..., update..., delete...方法轻松自如的进行CRUD操作了。
(3) 我们在使用的时候,还要通过sqlSession的getMapper()方法获得一个Mapper接口,然后再调用这个接口的crud方法:
@Test
public void testInterface() {
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtil.getSqlSessionFactory();
SqlSession session = sqlSessionFactory.openSession();
//可以直接映射到在命名空间中同名的 Mapper 类 (只要映射接口类就可以了,不需要实现)
UpmsUserDao upmsUserDao = session.getMapper(UpmsUserDao.class);
List<UpmsUser> upmsUsers = upmsUserDao.selectUser();
for (UpmsUser upmsUser : upmsUsers) {
System.out.println(upmsUser.toString());
}
}
这个getMapper()方法的执行流程其实是这样的:
DefaultSqlSession的getMapper()方法其实调用了Configuraiton类的getMapper()方法:
然后Configuration的内部又调用了MapperRegistry类的getMapper()方法:
然后MapperRegistry的内部其实是实例化了一个MapperProxyFactory对象:
MapperProxyFactory:
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//动态代理我们写的dao接口
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
所以SqlSession的getMapper()就是通过生成的这个动态代理MapperProxy对象,调用我们在Mapper接口中定义的方法了。
那么调用了接口之后,怎么具体执行sql语句的呢?
(4)执行Sql语句
上面,咱们拿到了MapperProxy, 每个MapperProxy对应一个dao接口, 那么咱们在使用的时候,MapperProxy是怎么做的呢?源码:
/**
* MapperProxy在执行时会触发此方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
//并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//如果是自己这个类定义的方法,二话不说,主要交给MapperMethod自己去管
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
也就是动态代理的invoke方法,内部又是交给了MapperMethod的execute()方法去执行:
/**
* 看着代码不少,不过其实就是先判断CRUD类型,然后根据类型去选择到底执行sqlSession中的哪个方法,绕了一圈,又转回sqlSession了
* @param sqlSession
* @param args
* @return
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
既然上面的这些crud方法又回到SqlSession里面执行了, 那么咱们就看看SqlSession的CRUD方法了,为了省事,还是就选择其中的一个方法来做分析吧。这儿,咱们选择了selectList方法:
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
//CRUD实际上是交给Excetor去处理, excutor其实也只是穿了个马甲而已,小样,别以为穿个马甲我就不认识你嘞!
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();
}
}
这时候可以看到咱们之前解析每条sql语句对应的MappedStatement对象就排上用场了。
其实是执行了executor的query()方法,然后,通过一层一层的调用,最终会来到doQuery方法, 这儿咱们就随便找个Excutor看看doQuery方法的实现吧,我这儿选择了SimpleExecutor:
public <E> List<E> 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);
stmt = prepareStatement(handler, ms.getStatementLog());
//StatementHandler封装了Statement, 让 StatementHandler 去处理
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
返回结果是交给handler.<E>query(stmt, resultHandler)去处理的,那继续看看StatementHandler 的一个实现类 PreparedStatementHandler(这也是我们最常用的,封装的是PreparedStatement), 看看它使怎么去处理的:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//到此,原形毕露, PreparedStatement, 这个大家都已经滚瓜烂熟了吧
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
//结果交给了ResultSetHandler 去处理
return resultSetHandler.<E> handleResultSets(ps);
}
PreparedStatement是jdbc自带的一个类,执行完它的execute之后,把结果交给resultSetHandler.<E> handleResultSets(ps);去处理,我们看一下DefaultResultSetHandler这个具体的实现类,它的handleResultSets()方法就是以往我们自己写的那套处理ps结果的代码:
Configuration类:
Configuration,你可以把它当成一个数据的大管家。MyBatis所有的配置信息都维持在Configuration对象之中,基本每个对象都会持有它的引用。下面是部分属性:
比如mapper_id 和 mapper文件的映射,mapper_id和返回值的映射,mapper_id和参数的映射。还有mapper注册表等等
public class Configuration {
//环境
protected Environment environment;
protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel = true;
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls;
protected boolean useActualParamName = true;
protected boolean returnInstanceForEmptyRow;
//日志信息的前缀
protected String logPrefix;
//日志接口
protected Class<? extends Log> logImpl;
//文件系统接口
protected Class<? extends VFS> vfsImpl;
//本地Session范围
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
//数据库类型
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
//延迟加载的方法
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(
Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
//默认执行语句超时
protected Integer defaultStatementTimeout;
//默认的执行器
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
//数据库ID
protected String databaseId;
//mapper注册表
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
//拦截器链
protected final InterceptorChain interceptorChain = new InterceptorChain();
//类型处理器
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
//类型别名
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
//语言驱动
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
//mapper_id 和 mapper文件的映射
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(
"Mapped Statements collection");
//mapper_id和缓存的映射
protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
//mapper_id和返回值的映射
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
//mapper_id和参数的映射
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
//资源列表
protected final Set<String> loadedResources = new HashSet<String>();
.......
}
总结一下,整个的处理流程:
参考:
- 源码注释:https://github.com/tuguangquan/mybatis
- 源码:https://github.com/mybatis/mybatis-3
- MyBatis的使用和配置:https://blog.csdn.net/e_zxq/article/details/86150667
- 深入浅出Mybatis系列(十)---SQL执行流程分析(源码篇):https://www.cnblogs.com/dongying/p/4142476.html
- Mybatis源码分析(一)Mybatis的架构设计简介:https://www.jianshu.com/p/a1edbf3214e6
- Mybatis源码分析(五)探究SQL语句的执行过程:https://www.jianshu.com/p/43f304e4b784