本文基于Mybatis3.2.0版本的代码。
1.org.apache.ibatis.mapping.MappedStatement
MappedStatement类在Mybatis框架中用于表示XML文件中一个sql语句节点,即一个<select />、<update />或者<insert />标签。Mybatis框架在初始化阶段会对XML配置文件进行读取,将其中的sql语句节点对象化为一个个MappedStatement对象。比如下面这个非常简单的XML mapper文件:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?> |
03 |
PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN" |
04 |
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
05 |
< mapper namespace = "mybatis.UserDao" > |
07 |
< cache type = "org.mybatis.caches.ehcache.LoggingEhcache" /> |
09 |
< resultMap id = "userResultMap" type = "UserBean" > |
10 |
< id property = "userId" column = "user_id" /> |
11 |
< result property = "userName" column = "user_name" /> |
12 |
< result property = "userPassword" column = "user_password" /> |
13 |
< result property = "createDate" column = "create_date" /> |
16 |
< select id = "find" parameterType = "UserBean" resultMap = "userResultMap" > |
19 |
< if test = "userName!=null
and userName!=''" > |
20 |
and
user_name = #{userName} |
22 |
< if test = "userPassword!=null
and userPassword!=''" > |
23 |
and
user_password = #{userPassword} |
25 |
< if test = "createDate
!=null" > |
26 |
and
create_date = #{createDate} |
32 |
< select id = "find2" parameterType = "UserBean" resultMap = "userResultMap" > |
35 |
< if test = "userName!=null
and userName!=''" > |
36 |
and
user_name = #{userName} |
38 |
< if test = "userPassword!=null
and userPassword!=''" > |
39 |
and
user_password = #{userPassword} |
41 |
< if test = "createDate
!=null" > |
42 |
and
create_date = #{createDate} |
Mybatis对这个文件的配置读取和解析后,会注册两个MappedStatement对象,分别对应其中id为find和find2的<select />节点,通过org.apache.ibatis.session.Configuration类中的getMappedStatement(String id)方法,可以检索到一个特定的MappedStatement。为了区分不同的Mapper文件中的sql节点,其中的String id方法参数,是以Mapper文件的namespace作为前缀,再加上该节点本身的id值。比如上面生成的两个MappedStatement对象在Mybatis框架中的唯一标识分别是mybatis.UserDao.find和mybatis.UserDao.find2。
打开MappedStatement对象的源码,看一下其中的私有属性。
01 |
public final class MappedStatement
{ |
03 |
private String
resource; |
04 |
private Configuration
configuration; |
06 |
private Integer
fetchSize; |
07 |
private Integer
timeout; |
08 |
private StatementType
statementType; |
09 |
private ResultSetType
resultSetType; |
10 |
private SqlSource
sqlSource; |
12 |
private ParameterMap
parameterMap; |
13 |
private List<ResultMap>
resultMaps; |
14 |
private boolean flushCacheRequired; |
15 |
private boolean useCache; |
16 |
private boolean resultOrdered; |
17 |
private SqlCommandType
sqlCommandType; |
18 |
private KeyGenerator
keyGenerator; |
19 |
private String[]
keyProperties; |
20 |
private String[]
keyColumns; |
21 |
private boolean hasNestedResultMaps; |
22 |
private String
databaseId; |
23 |
private Log
statementLog; |
24 |
private LanguageDriver
lang; |
26 |
private MappedStatement()
{ |
我们可以看到其中的属性基本上和xml元素的属性有对应关系,其中比较重要的有表示查询参数的ParameterMap对象,表示sql查询结果映射关系的ResultMap列表resultMaps,当然最重要的还是执行动态sql计算和获取的SqlSource对象。通过这些对象的通力合作,MappedStatement接受用户的查询参数对象,动态计算出要执行的sql语句,在数据库中执行sql语句后,再将取得的数据封装为JavaBean对象返回给用户。MappedStatement对象的这些功能,也体现出了Mybatis这个框架的核心价值,“根据用户提供的查询参数对象,动态执行sql语句,并将结果封装为Java对象”。
2.org.apache.ibatis.mapping.SqlSource
SqlSource是一个接口类,在MappedStatement对象中是作为一个属性出现的,它的代码如下:
01 |
package org.apache.ibatis.mapping; |
05 |
*
This bean represets the content of a mapped statement read from an XML file |
06 |
*
or an annotation. It creates the SQL that will be passed to the database out |
07 |
*
of the input parameter received from the user. |
10 |
public interface SqlSource
{ |
12 |
BoundSql
getBoundSql(Object parameterObject); |
SqlSource接口只有一个getBoundSql(Object parameterObject)方法,返回一个BoundSql对象。一个BoundSql对象,代表了一次sql语句的实际执行,而SqlSource对象的责任,就是根据传入的参数对象,动态计算出这个BoundSql,也就是说Mapper文件中的<if
/>节点的计算,是由SqlSource对象完成的。SqlSource最常用的实现类是DynamicSqlSource,来看一看它的代码:
01 |
package org.apache.ibatis.scripting.xmltags; |
05 |
import org.apache.ibatis.builder.SqlSourceBuilder; |
06 |
import org.apache.ibatis.mapping.BoundSql; |
07 |
import org.apache.ibatis.mapping.SqlSource; |
08 |
import org.apache.ibatis.session.Configuration; |
10 |
public class DynamicSqlSource implements SqlSource
{ |
12 |
private Configuration
configuration; |
13 |
private SqlNode
rootSqlNode; |
15 |
public DynamicSqlSource(Configuration
configuration, SqlNode rootSqlNode) { |
16 |
this .configuration
= configuration; |
17 |
this .rootSqlNode
= rootSqlNode; |
20 |
public BoundSql
getBoundSql(Object parameterObject) { |
21 |
DynamicContext
context = new DynamicContext(configuration,
parameterObject); |
22 |
rootSqlNode.apply(context); |
23 |
SqlSourceBuilder
sqlSourceParser = new SqlSourceBuilder(configuration); |
24 |
Class<?>
parameterType = parameterObject == null ?
Object. class :
parameterObject.getClass(); |
25 |
SqlSource
sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); |
26 |
BoundSql
boundSql = sqlSource.getBoundSql(parameterObject); |
27 |
for (Map.Entry<String,
Object> entry : context.getBindings().entrySet()) { |
28 |
boundSql.setAdditionalParameter(entry.getKey(),
entry.getValue()); |
其中的
1 |
rootSqlNode.apply(context); |
这句调用语句,启动了一个非常精密的递归实现的动态计算sql语句的过程,计算过程使用Ognl来根据传入的参数对象计算表达式,生成该次调用过程中实际执行的sql语句。
3.org.apache.ibatis.scripting.xmltags.DynamicContext
DynamicContext类中,有对传入的parameterObject对象进行“map”化处理的部分,也就是说,你传入的pojo对象,会被当作一个键值对数据来源来进行处理,读取这个pojo对象的接口,还是Map对象。从DynamicContext的源码中,能看到很明显的线索。
001 |
import java.util.HashMap; |
002 |
import java.util.Map; |
004 |
import ognl.OgnlException; |
005 |
import ognl.OgnlRuntime; |
006 |
import ognl.PropertyAccessor; |
008 |
import org.apache.ibatis.reflection.MetaObject; |
009 |
import org.apache.ibatis.session.Configuration; |
011 |
public class DynamicContext
{ |
013 |
public static final String
PARAMETER_OBJECT_KEY = "_parameter" ; |
014 |
public static final String
DATABASE_ID_KEY = "_databaseId" ; |
017 |
OgnlRuntime.setPropertyAccessor(ContextMap. class , new ContextAccessor()); |
020 |
private final ContextMap
bindings; |
021 |
private final StringBuilder
sqlBuilder = new StringBuilder(); |
022 |
private int uniqueNumber
= 0 ; |
024 |
public DynamicContext(Configuration
configuration, Object parameterObject) { |
025 |
if (parameterObject
!= null &&
!(parameterObject instanceof Map))
{ |
026 |
MetaObject
metaObject = configuration.newMetaObject(parameterObject); |
027 |
bindings
= new ContextMap(metaObject); |
029 |
bindings
= new ContextMap( null ); |
031 |
bindings.put(PARAMETER_OBJECT_KEY,
parameterObject); |
032 |
bindings.put(DATABASE_ID_KEY,
configuration.getDatabaseId()); |
035 |
public Map<String,
Object> getBindings() { |
039 |
public void bind(String
name, Object value) { |
040 |
bindings.put(name,
value); |
043 |
public void appendSql(String
sql) { |
044 |
sqlBuilder.append(sql); |
045 |
sqlBuilder.append( "
" ); |
048 |
public String
getSql() { |
049 |
return sqlBuilder.toString().trim(); |
052 |
public int getUniqueNumber()
{ |
053 |
return uniqueNumber++; |
056 |
static class ContextMap extends HashMap<String,
Object> { |
057 |
private static final long serialVersionUID
= 2977601501966151582L; |
059 |
private MetaObject
parameterMetaObject; |
060 |
public ContextMap(MetaObject
parameterMetaObject) { |
061 |
this .parameterMetaObject
= parameterMetaObject; |
065 |
public Object
get(Object key) { |
066 |
String
strKey = (String) key; |
067 |
if ( super .containsKey(strKey))
{ |
068 |
return super .get(strKey); |
071 |
if (parameterMetaObject
!= null )
{ |
072 |
Object
object = parameterMetaObject.getValue(strKey); |
073 |
if (object
!= null )
{ |
074 |
super .put(strKey,
object); |
084 |
static class ContextAccessor implements PropertyAccessor
{ |
086 |
public Object
getProperty(Map context, Object target, Object name) |
087 |
throws OgnlException
{ |
088 |
Map
map = (Map) target; |
090 |
Object
result = map.get(name); |
091 |
if (result
!= null )
{ |
095 |
Object
parameterObject = map.get(PARAMETER_OBJECT_KEY); |
096 |
if (parameterObject instanceof Map)
{ |
097 |
return ((Map)parameterObject).get(name); |
103 |
public void setProperty(Map
context, Object target, Object name, Object value) |
104 |
throws OgnlException
{ |
105 |
Map
map = (Map) target; |
106 |
map.put(name,
value); |
在DynamicContext的构造函数中,可以看到,根据传入的参数对象是否为Map类型,有两个不同构造ContextMap的方式。而ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。具体来说,当传入的参数对象不是Map类型时,Mybatis会将传入的POJO对象用MetaObject对象来封装,当动态计算sql过程需要获取数据时,用Map接口的get方法包装 MetaObject对象的取值过程。
我们都知道,Mybatis中采用了Ognl来计算动态sql语句,DynamicContext类中的这个静态初始块,很好的说明了这一点
2 |
OgnlRuntime.setPropertyAccessor(ContextMap. class , new ContextAccessor()); |
ContextAccessor也是DynamicContext的内部类,实现了Ognl中的PropertyAccessor接口,为Ognl提供了如何使用ContextMap参数对象的说明,这个类也为整个参数对象“map”化划上了最后一笔。
现在我们能比较清晰的描述一下Mybatis中的参数传递和使用过程了:将传入的参数对象统一封装为ContextMap对象(继承了HashMap对象),然后Ognl运行时环境在动态计算sql语句时,会按照ContextAccessor中描述的Map接口的方式来访问和读取ContextMap对象,获取计算过程中需要的参数。ContextMap对象内部可能封装了一个普通的POJO对象,也可以是直接传递的Map对象,当然从外部是看不出来的,因为都是使用Map的接口来读取数据。
结合一个例子来理解一下:
02 |
public void testSqlSource() throws Exception
{ |
03 |
String
resource = "mybatis/mybatis-config.xml" ; |
04 |
InputStream
inputStream = Resources.getResourceAsStream(resource); |
05 |
SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder() |
07 |
SqlSession
session = sqlSessionFactory.openSession(); |
10 |
Configuration
configuration = session.getConfiguration(); |
11 |
MappedStatement
mappedStatement = configuration |
12 |
.getMappedStatement( "mybatis.UserDao.find2" ); |
13 |
assertNotNull(mappedStatement); |
15 |
UserBean
param = new UserBean(); |
16 |
param.setUserName( "admin" ); |
17 |
param.setUserPassword( "admin" ); |
18 |
BoundSql
boundSql = mappedStatement.getBoundSql(param); |
19 |
String
sql = boundSql.getSql(); |
21 |
Map<String,
Object> map = new HashMap<String,
Object>(); |
22 |
map.put( "userName" , "admin" ); |
23 |
map.put( "userPassword" , "admin" ); |
24 |
BoundSql
boundSql2 = mappedStatement.getBoundSql(map); |
25 |
String
sql2 = boundSql2.getSql(); |
27 |
assertEquals(sql,
sql2); |
29 |
UserBean
bean = session.selectOne( "mybatis.UserDao.find2" ,
map); |
上面这个Junit测试方法,是我写的一个测试用例中的一小段,其中的UserBean对象,就是一个有三个属性userName,userPassword,createDate的POJO对象,对应的Mapper文件是文章开头给出的配置文件。
第一次测试,我使用的是一个UserBean对象,来获取和计算sql语句,而第二次我是使用了一个HashMap对象,按照属性的名字,我分别设置了两个键值对象,我甚至还直接使用它来启动了一次session对象的查询selectOne。所有这些操作,都是测试通过(绿条)。这充分说明了,Mybatis参数获取过程中,对Map对象和普通POJO对象的无差别化,因为在内部,两者都会被封装,然后通过Map接口来访问!
下一篇说说Mybatis中接口和Mapper文件是如何通过创建代理类来进行绑定的,这个特性也是Mybatis设计的一大亮点。