Mybatis的事务如何被Spring管理?
不知道一些同学有没有这种疑问,为什么Mybtis中要配置dataSource,Spring的事务中也要配置dataSource?那么Mybatis和Spring事务中用的Connection是同一个吗?我们常用配置如下
<!--会话工厂 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
<!--spring事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--使用注释事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
看到没,sqlSessionFactory中配置了dataSource,transactionManager也配置了dataSource,我们来回忆一下SqlSessionFactoryBean这个类
1 protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
2
3 // 配置类
4 Configuration configuration;
5 // 解析mybatis-Config.xml文件,
6 // 将相关配置信息保存到configuration
7 XMLConfigBuilder xmlConfigBuilder = null;
8 if (this.configuration != null) {
9 configuration = this.configuration;
10 if (configuration.getVariables() == null) {
11 configuration.setVariables(this.configurationProperties);
12 } else if (this.configurationProperties != null) {
13 configuration.getVariables().putAll(this.configurationProperties);
14 }
15 //资源文件不为空
16 } else if (this.configLocation != null) {
17 //根据configLocation创建xmlConfigBuilder,XMLConfigBuilder构造器中会创建Configuration对象
18 xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
19 //将XMLConfigBuilder构造器中创建的Configuration对象直接赋值给configuration属性
20 configuration = xmlConfigBuilder.getConfiguration();
21 }
22
23 //略....
24
25 if (xmlConfigBuilder != null) {
26 try {
27 //解析mybatis-Config.xml文件,并将相关配置信息保存到configuration
28 xmlConfigBuilder.parse();
29 if (LOGGER.isDebugEnabled()) {
30 LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
31 }
32 } catch (Exception ex) {
33 throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
34 }
35 }
36
37 if (this.transactionFactory == null) {
38 //事务默认采用SpringManagedTransaction,这一块非常重要
39 this.transactionFactory = new SpringManagedTransactionFactory();
40 }
41 // 为sqlSessionFactory绑定事务管理器和数据源
42 // 这样sqlSessionFactory在创建sqlSession的时候可以通过该事务管理器获取jdbc连接,从而执行SQL
43 configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
44 // 解析mapper.xml
45 if (!isEmpty(this.mapperLocations)) {
46 for (Resource mapperLocation : this.mapperLocations) {
47 if (mapperLocation == null) {
48 continue;
49 }
50 try {
51 // 解析mapper.xml文件,并注册到configuration对象的mapperRegistry
52 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
53 configuration, mapperLocation.toString(), configuration.getSqlFragments());
54 xmlMapperBuilder.parse();
55 } catch (Exception e) {
56 throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
57 } finally {
58 ErrorContext.instance().reset();
59 }
60
61 if (LOGGER.isDebugEnabled()) {
62 LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
63 }
64 }
65 } else {
66 if (LOGGER.isDebugEnabled()) {
67 LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
68 }
69 }
70
71 // 将Configuration对象实例作为参数,
72 // 调用sqlSessionFactoryBuilder创建sqlSessionFactory对象实例
73 return this.sqlSessionFactoryBuilder.build(configuration);
74 }
我们看第39行,Mybatis集成Spring后,默认使用的transactionFactory是SpringManagedTransactionFactory,那我们就来看看其获取Transaction的方法
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
boolean autoCommit;
try {
autoCommit = connection.getAutoCommit();
} catch (SQLException e) {
// Failover to true, as most poor drivers
// or databases won't support transactions
autoCommit = true;
}
//从configuration中取出environment对象
final Environment environment = configuration.getEnvironment();
//从environment中取出TransactionFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//创建Transaction
final Transaction tx = transactionFactory.newTransaction(connection);
//创建包含事务操作的执行器
final Executor executor = configuration.newExecutor(tx, execType);
//构建包含执行器的SqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
if (environment == null || environment.getTransactionFactory() == null) {
return new ManagedTransactionFactory();
}
//这里返回SpringManagedTransactionFactory
return environment.getTransactionFactory();
}
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
//创建SpringManagedTransaction
return new SpringManagedTransaction(dataSource);
}
SpringManagedTransaction
也就是说mybatis的执行事务的事务管理器就切换成了SpringManagedTransaction,下面我们再去看看SpringManagedTransactionFactory类的源码:
public class SpringManagedTransaction implements Transaction {
private static final Log LOGGER = LogFactory.getLog(SpringManagedTransaction.class);
private final DataSource dataSource;
private Connection connection;
private boolean isConnectionTransactional;
private boolean autoCommit;
public SpringManagedTransaction(DataSource dataSource) {
Assert.notNull(dataSource, "No DataSource specified");
this.dataSource = dataSource;
}
public Connection getConnection() throws SQLException {
if (this.connection == null) {
this.openConnection();
}
return this.connection;
}
private void openConnection() throws SQLException {
//通过DataSourceUtils获取connection,这里和JdbcTransaction不一样
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
}
}
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
}
//通过connection提交,这里和JdbcTransaction一样
this.connection.commit();
}
}
public void rollback() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
}
//通过connection回滚,这里和JdbcTransaction一样
this.connection.rollback();
}
}
public void close() throws SQLException {
DataSourceUtils.releaseConnection(this.connection, this.dataSource);
}
public Integer getTimeout() throws SQLException {
ConnectionHolder holder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.dataSource);
return holder != null && holder.hasTimeout() ? holder.getTimeToLiveInSeconds() : null;
}
}
org.springframework.jdbc.datasource.DataSourceUtils#getConnection
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
return doGetConnection(dataSource);
}
catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
//TransactionSynchronizationManager重点!!!有没有很熟悉的感觉??
//还记得我们前面Spring事务源码的分析吗?@Transaction会创建Connection,并放入ThreadLocal中
//这里从ThreadLocal中获取ConnectionHolder
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
logger.debug("Fetching JDBC Connection from DataSource");
//如果没有使用@Transaction,那调用Mapper接口方法时,也是通过Spring的方法获取Connection
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
ConnectionHolder holderToUse = conHolder;
if (conHolder == null) {
holderToUse = new ConnectionHolder(con);
} else {
conHolder.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
//将获取到的ConnectionHolder放入ThreadLocal中,那么当前线程调用下一个接口,下一个接口使用了Spring事务,那Spring事务也可以直接取到Mybatis创建的Connection
//通过ThreadLocal保证了同一线程中Spring事务使用的Connection和Mapper代理类使用的Connection是同一个
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
} else {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
//所以如果我们业务代码使用了@Transaction注解,在Spring中就已经通过dataSource创建了一个Connection并放入ThreadLocal中
//那么当Mapper代理对象调用方法时,通过SqlSession的SpringManagedTransaction获取连接时,就直接获取到了当前线程中Spring事务创建的Connection并返回
return conHolder.getConnection();
}
}
想看怎么获取connHolder
org.springframework.transaction.support.TransactionSynchronizationManager#getResource
//保存数据库连接的ThreadLocal
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
@Nullable
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
//获取ConnectionHolder
Object value = doGetResource(actualKey);
....
return value;
}
@Nullable
private static Object doGetResource(Object actualKey) {
/**
* 从threadlocal <Map<Object, Object>>中取出来当前线程绑定的map
* map里面存的是<dataSource,ConnectionHolder>
*/
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
//map中取出来对应dataSource的ConnectionHolder
Object value = map.get(actualKey);
// Transparently remove ResourceHolder that was marked as void...
if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
map.remove(actualKey);
// Remove entire ThreadLocal if empty...
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}
我们看到直接从ThreadLocal中取出来的conn,而spring自己的事务也是操作的这个ThreadLocal中的conn来进行事务的开启和回滚,由此我们知道了在同一线程中Spring事务中的Connection和Mybaits中Mapper代理对象中操作数据库的Connection是同一个,当取出来的conn为空时候,调用org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection获取,然后把从数据源取出来的连接返回
private static Connection fetchConnection(DataSource dataSource) throws SQLException {
//从数据源取出来conn
Connection con = dataSource.getConnection();
if (con == null) {
throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
}
return con;
}
我们再来回顾一下上篇文章中的SqlSessionInterceptor
1 private class SqlSessionInterceptor implements InvocationHandler {
2 private SqlSessionInterceptor() {
3 }
4
5 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
6 SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
7
8 Object unwrapped;
9 try {
10 Object result = method.invoke(sqlSession, args);
11 // 如果当前操作没有在一个Spring事务中,则手动commit一下
12 // 如果当前业务没有使用@Transation,那么每次执行了Mapper接口的方法直接commit
13 // 还记得我们前面讲的Mybatis的一级缓存吗,这里一级缓存不能起作用了,因为每执行一个Mapper的方法,sqlSession都提交了
14 // sqlSession提交,会清空一级缓存
15 if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
16 sqlSession.commit(true);
17 }
18
19 unwrapped = result;
20 } catch (Throwable var11) {
21 unwrapped = ExceptionUtil.unwrapThrowable(var11);
22 if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
23 SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
24 sqlSession = null;
25 Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
26 if (translated != null) {
27 unwrapped = translated;
28 }
29 }
30
31 throw (Throwable)unwrapped;
32 } finally {
33 if (sqlSession != null) {
34 SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
35 }
36
37 }
38 return unwrapped;
39 }
40 }
看第15和16行,如果我们没有使用@Transation,Mapper方法执行完后,sqlSession将会提交,也就是说通过org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection获取到的Connection将会commit,相当于Connection是自动提交的,也就是说如果不使用@Transation,Mybatis将没有事务可言。
Mybatis和Spring整合后SpringManagedTransaction和Spring的Transaction的关系:
- 如果开启Spring事务,则先有Spring的Transaction,然后mybatis创建sqlSession时,会创建SpringManagedTransaction并加入sqlSession中,SpringManagedTransaction中的connection会从Spring的Transaction创建的Connection并放入ThreadLocal中获取
- 如果没有开启Spring事务或者第一个方法没有事务后面的方法有事务,则SpringManagedTransaction创建Connection并放入ThreadLocal中
spring结合mybatis后mybaits一级缓存失效分为两种情况:
- 如果没有开启事务,每一次sql都是用的新的SqlSession,这时mybatis的一级缓存是失效的。
- 如果有事务,同一个事务中相同的查询使用的相同的SqlSessioon,此时一级缓存是生效的。
如果使用了@Transation呢?那在调用Mapper代理类的方法之前就已经通过Spring的事务生成了Connection并放入ThreadLocal,并且设置事务不自动提交,当前线程多个Mapper代理对象调用数据库操作方法时,将从ThreadLocal获取Spring创建的connection,在所有的Mapper方法调用完后,Spring事务提交或者回滚,到此mybatis的事务是怎么被spring管理的就显而易见了
还有文章开头的问题,为什么Mybtis中要配置dataSource,Spring的事务中也要配置dataSource?
因为Spring事务在没调用Mapper方法之前就需要开一个Connection,并设置事务不自动提交,那么transactionManager中自然要配置dataSource。那如果我们的Service没有用到Spring事务呢,难道就不需要获取数据库连接了吗?当然不是,此时通过SpringManagedTransaction调用org.springframework.jdbc.datasource.DataSourceUtils#getConnection#fetchConnection方法获取,并将dataSource作为参数传进去,实际上获取的Connection都是通过dataSource来获取的。
目录
))))))))))))))))))))))))))))))))))))))))))
API 接口层
会话 SqlSession
核心处理层
配置初始化
以 SqlSessionFactoryBuilder 去创建 SqlSessionFactory, 那么,我们就先从SqlSessionFactoryBuilder入手, 咱们先看看源码是怎么实现的
public class SqlSessionFactoryBuilder {
//Reader读取mybatis配置文件,传入构造方法
//除了Reader外,其实还有对应的inputStream作为参数的构造方法,
//这也体现了mybatis配置的灵活性
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}
//mybatis配置文件 + properties, 此时mybatis配置文件中可以不配置properties,也能使用${}形式
public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}
//通过XMLConfigBuilder解析mybatis配置,然后创建SqlSessionFactory对象
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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
通过源码,我们可以看到SqlSessionFactoryBuilder 通过XMLConfigBuilder 去解析我们传入的mybatis的配置文件, 下面就接着看看 XMLConfigBuilder 部分源码:
/**
* mybatis 配置文件解析
*/
public class XMLConfigBuilder extends BaseBuilder {
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
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;
}
//外部调用此方法对mybatis配置文件进行解析
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//从根节点configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//此方法就是解析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。
为了让大家能够更好地阅读mybatis源码,我先简单的给大家示例一下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>
那么,我要是 两种方法都同时用了,那么哪种方法优先?
当以上两种方法都xml配置优先, 外部指定properties配置其次。至于为什么,接下来的源码分析会提到,请留意一下。
再看一下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。 至于这个是怎么实现的, 下面源码就会讲。
好啦,上面简单给大家介绍了一下properties 和 environments 的配置, 接下来就正式开始看源码了:
上次我们说过mybatis 是通过XMLConfigBuilder这个类在解析mybatis配置文件的,那么本次就接着看看XMLConfigBuilder对于properties和environments的解析:
public class XMLConfigBuilder extends BaseBuilder {
private boolean parsed;
//xml解析器
private XPathParser parser;
private String environment;
//上次说到这个方法是在解析mybatis配置文件中能配置的元素节点
//今天首先要看的就是properties节点和environments节点
private void parseConfiguration(XNode root) {
try {
//解析properties元素
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"));
//解析environments元素
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);
}
}
//下面就看看解析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);
}
}
//下面再看看解析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.");
}
}
通过以上对mybatis源码的解读,相信大家对mybatis的配置又有了一个深入的认识。
还有一个问题, 上面我们看到,在配置dataSource的时候使用了 ${driver} 这种表达式, 这种形式是怎么解析的?其实,是通过PropertyParser这个类解析:
/**
* 这个类解析${}这种形式的表达式
*/
public class PropertyParser {
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
private static class VariableTokenHandler implements TokenHandler {
private Properties variables;
public VariableTokenHandler(Properties variables) {
this.variables = variables;
}
public String handleToken(String content) {
if (variables != null && variables.containsKey(content)) {
return variables.getProperty(content);
}
return "${" + content + "}";
}
}
}
好啦,以上就是对于properties 和 environments元素节点的分析,比较重要的都在对于源码的注释中标出。
例如: 我们在使用 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>
先从对xml的解析讲起:
/**
* 解析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);
}
}
}
}
}
重要的源码在这儿
public class TypeAliasRegistry {
//这就是核心所在啊, 原来别名就仅仅通过一个HashMap来实现, key为别名, value就是别名对应的类型(class对象)
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
/**
* 以下就是mybatis默认为我们注册的别名
*/
public TypeAliasRegistry() {
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class);
registerAlias("_byte", byte.class);
registerAlias("_long", long.class);
registerAlias("_short", short.class);
registerAlias("_int", int.class);
registerAlias("_integer", int.class);
registerAlias("_double", double.class);
registerAlias("_float", float.class);
registerAlias("_boolean", boolean.class);
registerAlias("_byte[]", byte[].class);
registerAlias("_long[]", long[].class);
registerAlias("_short[]", short[].class);
registerAlias("_int[]", int[].class);
registerAlias("_integer[]", int[].class);
registerAlias("_double[]", double[].class);
registerAlias("_float[]", float[].class);
registerAlias("_boolean[]", boolean[].class);
registerAlias("date", Date.class);
registerAlias("decimal", BigDecimal.class);
registerAlias("bigdecimal", BigDecimal.class);
registerAlias("biginteger", BigInteger.class);
registerAlias("object", Object.class);
registerAlias("date[]", Date[].class);
registerAlias("decimal[]", BigDecimal[].class);
registerAlias("bigdecimal[]", BigDecimal[].class);
registerAlias("biginteger[]", BigInteger[].class);
registerAlias("object[]", Object[].class);
registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
registerAlias("list", List.class);
registerAlias("arraylist", ArrayList.class);
registerAlias("collection", Collection.class);
registerAlias("iterator", Iterator.class);
registerAlias("ResultSet", ResultSet.class);
}
/**
* 处理别名, 直接从保存有别名的hashMap中取出即可
*/
@SuppressWarnings("unchecked")
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) return null;
String key = string.toLowerCase(Locale.ENGLISH); // issue #748
Class<T> value;
if (TYPE_ALIASES.containsKey(key)) {
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
/**
* 配置文件中配置为package的时候, 会调用此方法,根据配置的报名去扫描javabean ,然后自动注册别名
* 默认会使用 Bean 的首字母小写的非限定类名来作为它的别名
* 也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user)
*/
public void registerAliases(String packageName){
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType){
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for(Class<?> type : typeSet){
// Ignore inner classes and interfaces (including package-info.java)
// Skip also inner classes. See issue #6
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
public void registerAlias(Class<?> type) {
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
//这就是注册别名的本质方法, 其实就是向保存别名的hashMap新增值而已, 呵呵, 别名的实现太简单了,对吧
public void registerAlias(String alias, Class<?> value) {
if (alias == null) throw new TypeException("The parameter alias cannot be null");
String key = alias.toLowerCase(Locale.ENGLISH); // issue #748
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
}
TYPE_ALIASES.put(key, value);
}
public void registerAlias(String alias, String value) {
try {
registerAlias(alias, Resources.classForName(value));
} catch (ClassNotFoundException e) {
throw new TypeException("Error registering type alias "+alias+" for "+value+". Cause: " + e, e);
}
}
/**
* 获取保存别名的HashMap, Configuration对象持有对TypeAliasRegistry的引用,因此,如果需要,我们可以通过Configuration对象获取
*/
public Map<String, Class<?>> getTypeAliases() {
return Collections.unmodifiableMap(TYPE_ALIASES);
}
}
由源码可见,设置别名的原理就这么简单,Mybatis默认给我们设置了不少别名,在上面代码中都可以见到。
Mybatis中的TypeHandler是什么?
无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。Mybatis默认为我们实现了许多TypeHandler, 当我们没有配置指定TypeHandler时,Mybatis会根据参数或者返回结果的不同,默认为我们选择合适的TypeHandler处理。
那么,Mybatis为我们实现了哪些TypeHandler呢? 我们怎么自定义实现一个TypeHandler ? 这些都会在接下来的mybatis的源码中看到。
在看源码之前,还是像之前一样,先看看怎么配置吧?
<configuration>
<typeHandlers>
<!--
当配置package的时候,mybatis会去配置的package扫描TypeHandler
<package name="com.dy.demo"/>
-->
<!-- handler属性直接配置我们要指定的TypeHandler -->
<typeHandler handler=""/>
<!-- javaType 配置java类型,例如String, 如果配上javaType, 那么指定的typeHandler就只作用于指定的类型 -->
<typeHandler javaType="" handler=""/>
<!-- jdbcType 配置数据库基本数据类型,例如varchar, 如果配上jdbcType, 那么指定的typeHandler就只作用于指定的类型 -->
<typeHandler jdbcType="" handler=""/>
<!-- 也可两者都配置 -->
<typeHandler javaType="" jdbcType="" handler=""/>
</typeHandlers>
......
</configuration>
先从对xml的解析讲起
/**
* 解析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);
}
}
}
}
}
接下来看看TypeHandler的管理注册类:
/**
* typeHandler注册管理类
*/
public final class TypeHandlerRegistry {
//源码一上来,二话不说,几个大大的HashMap就出现,这不又跟上次讲的typeAliases的注册类似么
//基本数据类型与其包装类
private static final Map<Class<?>, Class<?>> reversePrimitiveMap = new HashMap<Class<?>, Class<?>>() {
private static final long serialVersionUID = 1L;
{
put(Byte.class, byte.class);
put(Short.class, short.class);
put(Integer.class, int.class);
put(Long.class, long.class);
put(Float.class, float.class);
put(Double.class, double.class);
put(Boolean.class, boolean.class);
put(Character.class, char.class);
}
};
//这几个MAP不用说就知道存的是什么东西吧,命名的好处
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler<?>>(JdbcType.class);
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<Type, Map<JdbcType, TypeHandler<?>>>();
private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<Class<?>, TypeHandler<?>>();
//就像上篇文章讲的typeAliases一样,mybatis也默认给我们注册了不少的typeHandler
//具体如下
public TypeHandlerRegistry() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());
register(Byte.class, new ByteTypeHandler());
register(byte.class, new ByteTypeHandler());
register(JdbcType.TINYINT, new ByteTypeHandler());
register(Short.class, new ShortTypeHandler());
register(short.class, new ShortTypeHandler());
register(JdbcType.SMALLINT, new ShortTypeHandler());
register(Integer.class, new IntegerTypeHandler());
register(int.class, new IntegerTypeHandler());
register(JdbcType.INTEGER, new IntegerTypeHandler());
register(Long.class, new LongTypeHandler());
register(long.class, new LongTypeHandler());
register(Float.class, new FloatTypeHandler());
register(float.class, new FloatTypeHandler());
register(JdbcType.FLOAT, new FloatTypeHandler());
register(Double.class, new DoubleTypeHandler());
register(double.class, new DoubleTypeHandler());
register(JdbcType.DOUBLE, new DoubleTypeHandler());
register(String.class, new StringTypeHandler());
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler());
register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
register(JdbcType.CHAR, new StringTypeHandler());
register(JdbcType.VARCHAR, new StringTypeHandler());
register(JdbcType.CLOB, new ClobTypeHandler());
register(JdbcType.LONGVARCHAR, new ClobTypeHandler());
register(JdbcType.NVARCHAR, new NStringTypeHandler());
register(JdbcType.NCHAR, new NStringTypeHandler());
register(JdbcType.NCLOB, new NClobTypeHandler());
register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
register(JdbcType.ARRAY, new ArrayTypeHandler());
register(BigInteger.class, new BigIntegerTypeHandler());
register(JdbcType.BIGINT, new LongTypeHandler());
register(BigDecimal.class, new BigDecimalTypeHandler());
register(JdbcType.REAL, new BigDecimalTypeHandler());
register(JdbcType.DECIMAL, new BigDecimalTypeHandler());
register(JdbcType.NUMERIC, new BigDecimalTypeHandler());
register(Byte[].class, new ByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
register(byte[].class, new ByteArrayTypeHandler());
register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.BLOB, new BlobTypeHandler());
register(Object.class, UNKNOWN_TYPE_HANDLER);
register(Object.class, JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
register(JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
register(Date.class, new DateTypeHandler());
register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
register(JdbcType.TIMESTAMP, new DateTypeHandler());
register(JdbcType.DATE, new DateOnlyTypeHandler());
register(JdbcType.TIME, new TimeOnlyTypeHandler());
register(java.sql.Date.class, new SqlDateTypeHandler());
register(java.sql.Time.class, new SqlTimeTypeHandler());
register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());
// issue #273
register(Character.class, new CharacterTypeHandler());
register(char.class, new CharacterTypeHandler());
}
public boolean hasTypeHandler(Class<?> javaType) {
return hasTypeHandler(javaType, null);
}
public boolean hasTypeHandler(TypeReference<?> javaTypeReference) {
return hasTypeHandler(javaTypeReference, null);
}
public boolean hasTypeHandler(Class<?> javaType, JdbcType jdbcType) {
return javaType != null && getTypeHandler((Type) javaType, jdbcType) != null;
}
public boolean hasTypeHandler(TypeReference<?> javaTypeReference, JdbcType jdbcType) {
return javaTypeReference != null && getTypeHandler(javaTypeReference, jdbcType) != null;
}
public TypeHandler<?> getMappingTypeHandler(Class<? extends TypeHandler<?>> handlerType) {
return ALL_TYPE_HANDLERS_MAP.get(handlerType);
}
public <T> TypeHandler<T> getTypeHandler(Class<T> type) {
return getTypeHandler((Type) type, null);
}
public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference) {
return getTypeHandler(javaTypeReference, null);
}
public TypeHandler<?> getTypeHandler(JdbcType jdbcType) {
return JDBC_TYPE_HANDLER_MAP.get(jdbcType);
}
public <T> TypeHandler<T> getTypeHandler(Class<T> type, JdbcType jdbcType) {
return getTypeHandler((Type) type, jdbcType);
}
public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference, JdbcType jdbcType) {
return getTypeHandler(javaTypeReference.getRawType(), jdbcType);
}
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
handler = jdbcHandlerMap.get(null);
}
}
if (handler == null && type != null && type instanceof Class && Enum.class.isAssignableFrom((Class<?>) type)) {
handler = new EnumTypeHandler((Class<?>) type);
}
@SuppressWarnings("unchecked")
// type drives generics here
TypeHandler<T> returned = (TypeHandler<T>) handler;
return returned;
}
public TypeHandler<Object> getUnknownTypeHandler() {
return UNKNOWN_TYPE_HANDLER;
}
public void register(JdbcType jdbcType, TypeHandler<?> handler) {
JDBC_TYPE_HANDLER_MAP.put(jdbcType, handler);
}
//
// REGISTER INSTANCE
//
/**
* 只配置了typeHandler, 没有配置jdbcType 或者javaType
*/
@SuppressWarnings("unchecked")
public <T> void register(TypeHandler<T> typeHandler) {
boolean mappedTypeFound = false;
//在自定义typeHandler的时候,可以加上注解MappedTypes 去指定关联的javaType
//因此,此处需要扫描MappedTypes注解
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> handledType : mappedTypes.value()) {
register(handledType, typeHandler);
mappedTypeFound = true;
}
}
// @since 3.1.0 - try to auto-discover the mapped type
if (!mappedTypeFound && typeHandler instanceof TypeReference) {
try {
TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
register(typeReference.getRawType(), typeHandler);
mappedTypeFound = true;
} catch (Throwable t) {
// maybe users define the TypeReference with a different type and are not assignable, so just ignore it
}
}
if (!mappedTypeFound) {
register((Class<T>) null, typeHandler);
}
}
/**
* 配置了typeHandlerhe和javaType
*/
public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
register((Type) javaType, typeHandler);
}
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
//扫描注解MappedJdbcTypes
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
register(javaType, null, typeHandler);
}
} else {
register(javaType, null, typeHandler);
}
}
public <T> void register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler) {
register(javaTypeReference.getRawType(), handler);
}
/**
* typeHandlerhe、javaType、jdbcType都配置了
*/
public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) {
register((Type) type, jdbcType, handler);
}
/**
* 注册typeHandler的核心方法
* 就是向Map新增数据而已
*/
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
if (map == null) {
map = new HashMap<JdbcType, TypeHandler<?>>();
TYPE_HANDLER_MAP.put(javaType, map);
}
map.put(jdbcType, handler);
if (reversePrimitiveMap.containsKey(javaType)) {
register(reversePrimitiveMap.get(javaType), jdbcType, handler);
}
}
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
//
// REGISTER CLASS
//
// Only handler type
public void register(Class<?> typeHandlerClass) {
boolean mappedTypeFound = false;
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> javaTypeClass : mappedTypes.value()) {
register(javaTypeClass, typeHandlerClass);
mappedTypeFound = true;
}
}
if (!mappedTypeFound) {
register(getInstance(null, typeHandlerClass));
}
}
// java type + handler type
public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}
// java type + jdbc type + handler type
public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
}
// Construct a handler (used also from Builders)
@SuppressWarnings("unchecked")
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
if (javaTypeClass != null) {
try {
Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
return (TypeHandler<T>) c.newInstance(javaTypeClass);
} catch (NoSuchMethodException ignored) {
// ignored
} catch (Exception e) {
throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
}
}
try {
Constructor<?> c = typeHandlerClass.getConstructor();
return (TypeHandler<T>) c.newInstance();
} catch (Exception e) {
throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
}
}
/**
* 根据指定的pacakge去扫描自定义的typeHander,然后注册
*/
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
//Ignore inner classes and interfaces (including package-info.java) and abstract classes
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
register(type);
}
}
}
// get information
/**
* 通过configuration对象可以获取已注册的所有typeHandler
*/
public Collection<TypeHandler<?>> getTypeHandlers() {
return Collections.unmodifiableCollection(ALL_TYPE_HANDLERS_MAP.values());
}
}
由源码可以看到, mybatis为我们实现了那么多TypeHandler, 随便打开一个TypeHandler,看其源码,都可以看到,它继承自一个抽象类:BaseTypeHandler, 那么我们是不是也能通过继承BaseTypeHandler,从而实现自定义的TypeHandler ? 答案是肯定的, 那么现在下面就为大家演示一下自定义TypeHandler:
@MappedJdbcTypes(JdbcType.VARCHAR)
//此处如果不用注解指定jdbcType, 那么,就可以在配置文件中通过"jdbcType"属性指定, 同理, javaType 也可通过 @MappedTypes指定
public class ExampleTypeHandler 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 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);
}
}
然后,就该配置我们的自定义TypeHandler了:
<configuration>
<typeHandlers>
<!-- 由于自定义的TypeHandler在定义时已经通过注解指定了jdbcType, 所以此处不用再配置jdbcType -->
<typeHandler handler="ExampleTypeHandler"/>
</typeHandlers>
......
</configuration>
也就是说,我们在自定义TypeHandler的时候,可以在TypeHandler通过@MappedJdbcTypes指定jdbcType, 通过 @MappedTypes 指定javaType, 如果没有使用注解指定,那么我们就需要在配置文件中配置。
那么,接下来,就简单介绍一下这几个objectFactory、databaseIdProvider、plugins、mappers配置的作用吧:
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
2、plugin有何作用? 需要配置吗?
plugins 是一个可选配置。mybatis中的plugin其实就是个interceptor, 它可以拦截Executor 、ParameterHandler 、ResultSetHandler 、StatementHandler 的部分方法,处理我们自己的逻辑。Executor就是真正执行sql语句的东西, ParameterHandler 是处理我们传入参数的,还记得前面讲TypeHandler的时候提到过,mybatis默认帮我们实现了不少的typeHandler, 当我们不显示配置typeHandler的时候,mybatis会根据参数类型自动选择合适的typeHandler执行,其实就是ParameterHandler 在选择。ResultSetHandler 就是处理返回结果的。
怎么自定义plugin ? 怎么配置?
要自定义一个plugin, 需要去实现Interceptor接口, 这儿不细说, 后面实战部分会详细讲解。定义好之后,配置如下:
<configuration>
......
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
......
</configuration>
3、mappers, 这下引出mybatis的核心之一了,mappers作用 ? 需要配置吗?
mappers 节点下,配置我们的mapper映射文件, 所谓的mapper映射文件,就是让mybatis 用来建立数据表和javabean映射的一个桥梁。在我们实际开发中,通常一个mapper文件对应一个dao接口, 这个mapper可以看做是dao的实现。所以,mappers必须配置。
那么怎么配置呢?
<configuration>
......
<mappers>
<!-- 第一种方式:通过resource指定 -->
<mapper resource="com/dy/dao/userDao.xml"/>
<!-- 第二种方式, 通过class指定接口,进而将接口与对应的xml文件形成映射关系
不过,使用这种方式必须保证 接口与mapper文件同名(不区分大小写),
我这儿接口是UserDao,那么意味着mapper文件为UserDao.xml
<mapper class="com.dy.dao.UserDao"/>
-->
<!-- 第三种方式,直接指定包,自动扫描,与方法二同理
<package name="com.dy.dao"/>
-->
<!-- 第四种方式:通过url指定mapper文件位置
<mapper url="file://........"/>
-->
</mappers>
......
</configuration>
这几个节点的解析源码,与之前提到的那些节点的解析类似
/**
* 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);
}
}
}
/**
* mappers 节点解析
* 这是mybatis的核心之一,这儿先简单介绍,在接下来的文章会对它进行分析
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//如果mappers节点的子节点是package, 那么就扫描package下的文件, 注入进configuration
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//resource, url, class 三选一
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//mapper映射文件都是通过XMLMapperBuilder解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
mapper映射文件的配置, 这是mybatis的核心之一,一定要学好。在mapper文件中,以mapper作为根节点,其下面可以配置的元素节点有: select, insert, update, delete, cache, cache-ref, resultMap, sql 。
本篇文章将简单介绍 insert, update, delete 的配置及使用,以后会对mybatis的源码进行深入讲解。
相信,看到insert, update, delete, 我们就知道其作用了,顾名思义嘛,myabtis 作为持久层框架,必须要对CRUD啊。
好啦,咱们就先来看看 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>
以上就是一个模板配置, 哪些是必要配置,哪些是根据自己实际需求,看一眼就知道了。
<?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="com.dy.dao.UserDao">
<!-- 对应userDao中的insertUser方法, -->
<insert id="insertUser" parameterType="com.dy.entity.User">
insert into user(id, name, password, age, deleteFlag)
values(#{id}, #{name}, #{password}, #{age}, #{deleteFlag})
</insert>
<!-- 对应userDao中的updateUser方法 -->
<update id="updateUser" parameterType="com.dy.entity.User">
update user set name = #{name}, password = #{password}, age = #{age}, deleteFlag = #{deleteFlag}
where id = #{id};
</update>
<!-- 对应userDao中的deleteUser 方法 -->
<delete id="deleteUser" parameterType="com.dy.entity.User">
delete from user where id = #{id};
</delete>
</mapper>
这样,一个简单的映射关系就建立了。仔细观察上面parameterType, “com.dy.entity.User”,包名要是再长点呢,每次都这样写,写得蛋疼了。别忘了之前讲的 typeAliases(别名), 那么这个地方,用上别名,岂不是技能跟蛋疼的长长的包名说拜拜了。好啦,咱们配上别名,在哪儿配? 当然是在mybatis 的全局配置文件(我这儿名字是mybatis-conf.xml), 不要认为是在mapper的配置文件里面配置哈。
mybatis-conf.xml:
<typeAliases>
<!--
通过package, 可以直接指定package的名字, mybatis会自动扫描你指定包下面的javabean,
并且默认设置一个别名,默认的名字为: javabean 的首字母小写的非限定类名来作为它的别名。
也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user)
<package name="com.dy.entity"/>
-->
<typeAlias alias="user" type="com.dy.entity.User"/>
</typeAliases>
这样,一个别名就取好了,咱们可以把上面的 com.dy.entity.User 都直接改为user 了。 这多方便呀!
我这儿数据库用的是mysql, 我把user表的主键id 设置了自动增长, 以上代码运行正常, 那么问题来了(当然,我不是要问学挖掘机哪家强),我要是换成oracle数据库怎么办? oracle 可是不支持id自增长啊? 怎么办?请看下面:
<!-- 对应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>
-->
insert into user(id, name, password, age, deleteFlag)
values(#{id}, #{name}, #{password}, #{age}, #{deleteFlag})
</insert>
同理,如果我们在使用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 语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 -->
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">
select无疑是我们最常用,也是最复杂的,mybatis通过resultMap能帮助我们很好地进行高级映射。下面就开始看看select 以及 resultMap的用法:
<select
<!-- 1. id (必须配置)
id是命名空间中的唯一标识符,可被用来代表这条语句。
一个命名空间(namespace) 对应一个dao接口,
这个id也应该对应dao里面的某个方法(相当于方法的实现),因此id 应该与方法名一致 -->
id="selectPerson"
<!-- 2. parameterType (可选配置, 默认为mybatis自动选择处理)
将要传入语句的参数的完全限定类名或别名, 如果不配置,mybatis会通过ParameterHandler 根据参数类型默认选择合适的typeHandler进行处理
parameterType 主要指定参数类型,可以是int, short, long, string等类型,也可以是复杂类型(如对象) -->
parameterType="int"
<!-- 3. resultType (resultType 与 resultMap 二选一配置)
resultType用以指定返回类型,指定的类型可以是基本类型,可以是java容器,也可以是javabean -->
resultType="hashmap"
<!-- 4. resultMap (resultType 与 resultMap 二选一配置)
resultMap用于引用我们通过 resultMap标签定义的映射类型,这也是mybatis组件高级复杂映射的关键 -->
resultMap="personResultMap"
<!-- 5. flushCache (可选配置)
将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false -->
flushCache="false"
<!-- 6. useCache (可选配置)
将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true -->
useCache="true"
<!-- 7. timeout (可选配置)
这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)-->
timeout="10000"
<!-- 8. fetchSize (可选配置)
这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动)-->
fetchSize="256"
<!-- 9. statementType (可选配置)
STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED-->
statementType="PREPARED"
<!-- 10. resultSetType (可选配置)
FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)-->
resultSetType="FORWARD_ONLY">
不过有个问题值得思考:在mybatis中如何处理这种一对多, 甚至于多对多,一对一的关系呢?
这儿,就不得不提到 resultMap 这个东西, mybatis的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>
本文将介绍mybatis强大的动态SQL。
那么,问题来了: 什么是动态SQL? 动态SQL有什么作用?
传统的使用JDBC的方法,相信大家在组合复杂的的SQL语句的时候,需要去拼接,稍不注意哪怕少了个空格,都会导致错误。Mybatis的动态SQL功能正是为了解决这种问题, 其通过 if, choose, when, otherwise, trim, where, set, foreach标签,可组合成非常灵活的SQL语句,从而提高开发人员的效率。下面就去感受Mybatis动态SQL的魅力吧:
1. if: 你们能判断,我也能判断!
作为程序猿,谁不懂 if ! 在mybatis中也能用 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, 有了我,SQL语句拼接条件神马的都是浮云!
咱们通过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是对它做了处理,当它遇到AND或者OR这些,它知道怎么处理。其实我们可以通过 trim 标签去自定义这种处理规则。
3. trim : 我的地盘,我做主!
上面的where标签,其实用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: 你有for, 我有foreach, 不要以为就你才屌!
java中有for, 可通过for循环, 同样, mybatis中有foreach, 可通过它实现循环,循环的对象当然主要是java容器和数组。
<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: 我选择了你,你选择了我!
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的那个。
纵观mybatis的动态SQL, 强大而简单, 相信大家简单看一下就能使用了。
好啦,本次就写到这!下篇文章将结合mybatis的源码分析一次sql语句执行的整个过程。
SQL 执行流程
SqlSessionFactory 与 SqlSession.
通过前面的章节对于mybatis 的介绍及使用,大家都能体会到SqlSession的重要性了吧, 没错,从表面上来看,咱们都是通过SqlSession去执行sql语句(注意:是从表面看,实际的待会儿就会讲)。那么咱们就先看看是怎么获取SqlSession的吧:
首先,SqlSessionFactoryBuilder去读取mybatis的配置文件,然后build一个DefaultSqlSessionFactory。源码如下:
/**
* 一系列的构造方法最终都会调用本方法(配置文件为Reader时会调用本方法,还有一个InputStream方法与此对应)
* @param reader
* @param environment
* @param properties
* @return
*/
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//通过XMLConfigBuilder解析配置文件,解析的配置相关信息都会封装为一个Configuration对象
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//这儿创建DefaultSessionFactory对象
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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
当我们获取到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语句咯)。看了上面,咱们也回想一下之前写的Demo,
SqlSessionFactory sessionFactory = null;
String resource = "mybatis-conf.xml";
try {
//SqlSessionFactoryBuilder读取配置文件
sessionFactory = new SqlSessionFactoryBuilder().build(Resources
.getResourceAsReader(resource));
} catch (IOException e) {
e.printStackTrace();
} //通过SqlSessionFactory获取SqlSessionSqlSession sqlSession = sessionFactory.openSession();
还真这么一回事儿,对吧!
SqlSession咱们也拿到了,咱们可以调用SqlSession中一系列的select…, insert…, update…, delete…方法轻松自如的进行CRUD操作了。 就这样? 那咱配置的映射文件去哪儿了? 别急, 咱们接着往下看:
利器之MapperProxy:
在mybatis中,通过MapperProxy动态代理咱们的dao, 也就是说, 当咱们执行自己写的dao里面的方法的时候,其实是对应的mapperProxy在代理。那么,咱们就看看怎么获取MapperProxy对象吧:
通过SqlSession从Configuration中获取。源码如下:
/**
* 什么都不做,直接去configuration中找, 哥就是这么任性
*/
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
SqlSession把包袱甩给了Configuration, 接下来就看看Configuration。源码如下:
/**
* 烫手的山芋,俺不要,你找mapperRegistry去要
* @param type
* @param sqlSession
* @return
*/
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
Configuration不要这烫手的山芋,接着甩给了MapperRegistry, 那咱看看MapperRegistry。 源码如下:
/**
* 烂活净让我来做了,没法了,下面没人了,我不做谁来做
* @param type
* @param sqlSession
* @return
*/
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//能偷懒的就偷懒,俺把粗活交给MapperProxyFactory去做
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//关键在这儿
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
MapperProxyFactory是个苦B的人,粗活最终交给它去做了。咱们看看源码:
/**
* 别人虐我千百遍,我待别人如初恋
* @param mapperProxy
* @return
*/
@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);
}
通过以上的动态代理,咱们就可以方便地使用dao接口啦, 就像之前咱们写的demo那样:
UserDao userMapper = sqlSession.getMapper(UserDao.class);
User insertUser = new User();
这下方便多了吧, 呵呵, 貌似mybatis的源码就这么一回事儿啊。
别急,还没完, 咱们还没看具体是怎么执行sql语句的呢。
接下来,咱们才要真正去看sql的执行过程了。
上面,咱们拿到了MapperProxy, 每个MapperProxy对应一个dao接口, 那么咱们在使用的时候,MapperProxy是怎么做的呢? 源码奉上:
/**
* MapperProxy在执行时会触发此方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
//二话不说,主要交给MapperMethod自己去管
return mapperMethod.execute(sqlSession, args);
}
MapperMethod
/**
* 看着代码不少,不过其实就是先判断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;
}
既然又回到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();
}
}
然后,通过一层一层的调用,最终会来到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);
}
}
接下来,咱们看看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);
}
到此, 一次sql的执行流程就完了。 我这儿仅抛砖引玉,建议有兴趣的去看看Mybatis3的源码。
参数设置
Mybatis框架极大地简化了ORM,让使用者自定义sql语句,再将查询结果映射到Java类中,其中很关键的一部分就是,将用户写的sql语句(以xml或者注解形式)解析成数据库可执行的sql语句。
本文将会介绍这整个的解析过程。
本文将以一下的注解定义为例,剖析整个解析过程。
@Select({"<script>",
"SELECT account",
"FROM user",
"WHERE id IN",
"<foreach item='item' index='index' collection='list'",
"open='(' separator=',' close=')'>",
"#{item}",
"</foreach>",
"</script>"})
List<String> selectAccountsByIds(@Param("list") int[] ids);
Sql解析其实分成了两部分,第一部分是将注解/xml中定义的sql语句转化成内存中的MappedStatement,也就是将script部分转化成内存中的MappedStatement,这部分发生在Mybatis初始化的时候,第二部分则是根据MappedStatement以及传入的参数,生成可以直接执行的sql语句,这部分发生在mapper函数被调用的时候。 从注解/xml 定义到MappedStatement
首先,我们要明白什么是MappedStatement,先来看看MappedStatment的类定义。
public final class MappedStatement {
private String resource; //来源,一般为文件名或者是注解的类名
private Configuration configuration; //Mybatis的全局唯一Configuration
private String id; //标志符,可以用于缓存
private Integer fetchSize; //每次需要查询的行数(可选)
private Integer timeout;//超时时间
private StatementType statementType;//语句类型,决定最后将使用Statement, PreparedStatement还是CallableStatement进行查询
private ResultSetType resultSetType;//结果集的读取类型,与java.sql.ResultSet中的类型对应。
private SqlSource sqlSource;//Mybatis中的sqlSource,保存了初次解析的结果
private Cache cache;//缓存空间
private ParameterMap parameterMap;//保存了方法参数与sql语句中的参数对应关系
private List<ResultMap> resultMaps;//可选,定义结果集与Java类型中的字段映射关系
private boolean flushCacheRequired;//是否立即写入
private boolean useCache;//是否使用缓存
private boolean resultOrdered;//可选,默认为false
private SqlCommandType sqlCommandType;//Sql执行类型(增、删、改、查)
private KeyGenerator keyGenerator;//可选,键生成器
private String[] keyProperties;//可选,作为键的属性
private String[] keyColumns;//可选,键的列
private boolean hasNestedResultMaps;//是否有嵌套的映射关系
private String databaseId;//数据库的id
private Log statementLog;//logger
private LanguageDriver lang;//解析器
private String[] resultSets;//可选,数据集的名称
}
可以说,MappedStatement里面包含了Mybatis有关Sql执行的所有特性,在这里我们能够找到Mybatis在Sql执行层面支持哪些特性,比如说可以支持定制化的数据库列到Java属性的映射,定制化的主键生成器。
接下来,我们看一些具体的处理逻辑,也是比较有意思的部分。
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
在XMLLanguageDriver中,真正发生了解析我们定义在XML/注解中的语句,这里的解析分成了两部分,第一部分,初始化一个Parser,第二部分,对Parser解析出来的节点再进行下一步解析。
而最后生成的SqlSource也分成了两个种,分别为RawSqlSource以及DynamicSqlSource。这样做的好处是,RawSqlSource可以在参数替换完成后直接执行,更加简单高效。
接下来再来看看解析具体Node的方法,XMLScriptBuilder
List<SqlNode> parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
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
String nodeName = child.getNode().getNodeName();
//使用具体的Handler进行处理
NodeHandler handler = nodeHandlers(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return contents;
}
所以,由上可知,最后我们写在Sql定义中的</foreach>这样的元素,会由实际的handler(ForEachHandler)来进行处理,并且最后也会生成特定的SqlNode用于动态Sql的生成。这样就确保了Mybatis对于元素可扩展性的支持,同时保证了Sql的动态化。
以上,就是从我们的定义,到MappedStatement的整个过程,在这个过程里面,结构化的XML语言被转化成了具体内存里面的一个个SqlNode,而像ResultMap这样的注解,则被转化成了MappedStatement里面其它的具体属性。
接下来,我们将会使用MappedStatement,进行可执行的Sql生成。
从MappedStatement到可执行的Sql
从MappedStatement到可执行的Sql部分相对来说就比较简单了,里面只涉及到从RawSqlSource/DynamicSqlSource到StaticSqlSource的转换。
而在DynamicSqlSource
中具体代码如下:
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
//首先,通过node的apply方法,动态生成占位符,以及相应的参数映射关系
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
//然后,再使用SqlSourceBuilder转化成staticSqlSource,这里也会对参数映射关系进行解析
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
//这里的bouldSql中的sql就已经是可执行的sql了
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
在这里,我们能够看到之前由XMLNode转化而成的SqlNode起作用了,通过其中的Apply方法,会根据参数的长度来添加动态的?占位符。
而返回的StaticSqlSource将会交由Executor进行包含参数设定,执行结构映射成Java实例在内的一系列操作,这个将会在后续的文章中继续解析。 通过MethodSignature将参数转化为Map
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
//首先,根据@param中注解的值,将相关参数添加到Map中
param.put(entry.getValue(), args[entry.getKey()]);
// 然后,再将参数的通用名,也添加到Map中
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
通过以上的转换之后,我们在后续的函数调用中,就可以以键值对的形式来获取方法参数的值了。
DynamicSqlSource中的参数解析 在上一片文章中,我们提到过,SqlNode中的apply方法,会将我们定义在注解中的Sql,转化成带有占位符的Sql。而实际上,除了Sql的转化之外,它还会向DynamicContext中添加Binding(参数值与对应键的绑定)。
ForeachSqlNode
中的apply
方法
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
//首先,通过解析器,将参数中的值解析成为一个可遍历的集合。
//初始状态下,bindins中只有_parameter -> { 'list' -> [1,2]}
//提取出来后的集合为[1,2]
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first) {
context = new PrefixedContext(context, "");
} else if (separator != null) {
context = new PrefixedContext(context, separator);
} else {
context = new PrefixedContext(context, "");
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
//将对应的binding添加到DynamicContext中。
//第一次遍历时,增加到binding 为 { '__frch_index_0' -> 0 }
applyIndex(context, i, uniqueNumber);
//第一次遍历时,增加到binding 为 { '__frch_item_0' -> 1 }
applyItem(context, o, uniqueNumber);
}
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
return true;
}
当SqlNode解析完成后,我们得到的DynamicContext中Sql如下:
SELECT account FROM user WHERE id IN ( #{__frch_item_0} , #{__frch_item_1} )
而在DynamicContext中的binding,则如下所示:
'__frch_index_0' -> 0,
'__frch_item_0' -> 1,
'__frch_index_1' -> 1,
'__frch_item_0' -> 2,
'_parameter' -> { 'list' -> [1,2] , 'param1' ->[1,2] },
'_databaseID' -> null
SqlSourceBuilder中获取ParameterMapping
虽然在DynamicContext中已经有了Bindings,但是Mybatis并不会直接使用这些binding进行查询。它会从含有占位符的语句中提取ParameterMapping关系,然后再根据ParameterMapping来对参数进行设置。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
//ParameterMappingTokenHandler,在查找到特定的token之后,对token进行处理,并且返回处理后的字符串
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
//只负责找到被#{}包围的字符串,然后交由tokenHandler进行处理
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
ParameterMappingTokenHandler
public String handleToken(String content) {
//首先,使用token中的标志符构造ParameterMapping,然后返回"?"
parameterMappings.add(buildParameterMapping(content));
return "?";
}
而在这一次处理之后,我们就能够能到可以被直接执行的Sql了。
SELECT account FROM user WHERE id IN ( ? , ? )
同时,也生成了对应的ParameterMapping(暂时忽略一些其它属性)
[
{ property: '__frch_item_0'},
{ property: '__frch_item_1'}
]
最后,在构造BoundSql时,Mybatis还会做如下的事情:
//将默认的参数,也作为BoundSql的默认参数
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//在context中生成的binding,则会做为额外的参数,也传给boundSql
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
参数的使用——DefaultParameterHandler
在这个类里面,我们只需要关注一个方法setParameters
。
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
//parameterMapping是根据Sql中占位符逐个生成的,因此数组中的顺序也等同于sql中对应参数的顺序,直接进行遍历以及参数设置即可。
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)) {
// 首先,从额外的参数中获取参数值,获取顺序很重要,这个与Mybatis中的issue #448有关
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
//然后,再从parameterObject中进行获取。
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//考虑到Java类型与Mysql类型还有一个映射关系,所以使用typeHandler进行处理
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
主键生成
正常情况下insert语句不会返回插入后所生成的主键,返回的是受影响的记录数。如果在插入一条数据后希望能得到数据库生成的主键(无论是oracle的自增序列,还是mysql的自增主键),则可以使用KeyGenerator接口。
使用方式为在<insert/>
或<update/>
标签上配置useGeneratedKeys属性,关于useGeneratedKeys、keyProperty和keyColumn属性,官方文档对其的描述为:
useGeneratedKeys | (仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。 |
---|---|
keyProperty | (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset )。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
keyColumn | (仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
Mapper.xml
<mapper namespace="userMapper">
<insert id="addUser" parameterType="userScope" useGeneratedKeys="true" keyProperty="id">
insert user(user_name,pass_word,address) values (#{userName},#{passWord},#{address})
</insert>
<insert id="insertSelectKey" parameterType="userScope">
<selectKey keyProperty="id" resultType="int">
select LAST_INSERT_ID()
</selectKey>
insert user(user_name,pass_word,address) values (#{userName},#{passWord},#{address})
</insert>
</mapper>
上面写了两种方式用来获取自增长主键。一种是使用 *useGemeratedKeys*,一种是使用 *
PreparedStatementHandler
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();//这里是调用的JDBC4PreparedStatement执行sql
int rows = ps.getUpdateCount();//获取受影响的行数,也就是调用插入操作的时候返回的真正值。
Object parameterObject = boundSql.getParameterObject();//获取参数对象
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();//判断是那种KeyGenerator
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);//根据KeyGenerator去set主键值
return rows;
}
KeyGenerator接口的定义如下:
public interface KeyGenerator {
//在sql执行之前
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
//在sql执行之后
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
KeyGenerator有三个实现类:NoKeyGenerator、Jdbc3KeyGenerator、SelectKeyGenerator。
NoKeyGenerator为空实现,没有设置返回自增长主键值的插入操作
Jdbc3KeyGenerator用于在执行sql的时候取回生成的id,并将其保存在用户传入的实参中,
SelectKeyGenerator用于当前使用的数据库不支持主键自增的场景,用户自己编写sql语句来生成对应的自增id。
下面我们来分析Jdbc3KeyGenerator这个类。
public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
ResultSet rs = null;
try {
rs = stmt.getGeneratedKeys();//获取执行结果值
final Configuration configuration = ms.getConfiguration();
final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
final String[] keyProperties = ms.getKeyProperties();//获取设置的keyProperty
final ResultSetMetaData rsmd = rs.getMetaData();
TypeHandler<?>[] typeHandlers = null;
if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {//如果设置了keyProperty,则在参数对象中放入处理之后的结果值
for (Object parameter : parameters) {
// there should be one row for each statement (also one for each parameter)
if (!rs.next()) {
break;
}
final MetaObject metaParam = configuration.newMetaObject(parameter);
if (typeHandlers == null) {
typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);//keyProperty设置的字段类型的处理器
}
populateKeys(rs, metaParam, keyProperties, typeHandlers);//在参数对象中设置keyProperty的字段值
}
}
private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
for (int i = 0; i < keyProperties.length; i++) {
TypeHandler<?> th = typeHandlers[i];
if (th != null) {
Object value = th.getResult(rs, i + 1);
metaParam.setValue(keyProperties[i], value);//在参数对象中设置keyProperty的字段值
}
}
}
public void setValue(String name, Object value) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
if (value == null && prop.getChildren() != null) {
// don't instantiate child path if value is null
return;
} else {
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
metaValue.setValue(prop.getChildren(), value);
} else {
objectWrapper.set(prop, value);//在参数对象中设置keyProperty的字段值(根据反射来操作的)
}
}
MyBatis3.4.0之后已经支持批量插入并获取自增主键值了。例子如下:
public void addUserBatch(List<UserScope> list) {
this.getSqlSession().insert("userMapper.addUserBatch",list);
System.out.println(Arrays.toString(list.toArray()));
}
<insert id="addUserBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
insert user(user_name,pass_word,address) values
<foreach collection="list" item="item" separator=",">
( #{item.userName},#{item.passWord},#{item.address} )
</foreach>
</insert>