MyBatis源码分析之MappedStatemenet,SqlSource,DynamicContext

转载 2015年07月08日 14:41:47

本文基于Mybatis3.2.0版本的代码。

1.org.apache.ibatis.mapping.MappedStatement

MappedStatement类在Mybatis框架中用于表示XML文件中一个sql语句节点,即一个<select />、<update />或者<insert />标签。Mybatis框架在初始化阶段会对XML配置文件进行读取,将其中的sql语句节点对象化为一个个MappedStatement对象。比如下面这个非常简单的XML mapper文件:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xmlversion="1.0"encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mappernamespace="mybatis.UserDao">
 
    <cachetype="org.mybatis.caches.ehcache.LoggingEhcache"/>
 
    <resultMapid="userResultMap"type="UserBean">
        <idproperty="userId"column="user_id"/>
        <resultproperty="userName"column="user_name"/>
        <resultproperty="userPassword"column="user_password"/>
        <resultproperty="createDate"column="create_date"/>
    </resultMap>
 
    <selectid="find"parameterType="UserBean"resultMap="userResultMap">
        select * from user
        <where>
            <iftest="userName!=null and userName!=''">
                and user_name = #{userName}
            </if>
            <iftest="userPassword!=null and userPassword!=''">
                and user_password = #{userPassword}
            </if>
            <iftest="createDate !=null">
                and create_date = #{createDate}
            </if>
        </where>
    </select>
 
    <!-- 说明mybatis中的sql语句节点和映射的接口中的方法,并不是一一对应的关系,而是独立的,可以取任意不重复的名称 -->
    <selectid="find2"parameterType="UserBean"resultMap="userResultMap">
        select * from user
        <where>
            <iftest="userName!=null and userName!=''">
                and user_name = #{userName}
            </if>
            <iftest="userPassword!=null and userPassword!=''">
                and user_password = #{userPassword}
            </if>
            <iftest="createDate !=null">
                and create_date = #{createDate}
            </if>
        </where>
    </select>
 
</mapper>

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对象的源码,看一下其中的私有属性。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
publicfinal class MappedStatement {
 
  privateString resource;
  privateConfiguration configuration;
  privateString id;
  privateInteger fetchSize;
  privateInteger timeout;
  privateStatementType statementType;
  privateResultSetType resultSetType;
  privateSqlSource sqlSource;
  privateCache cache;
  privateParameterMap parameterMap;
  privateList<ResultMap> resultMaps;
  privateboolean flushCacheRequired;
  privateboolean useCache;
  privateboolean resultOrdered;
  privateSqlCommandType sqlCommandType;
  privateKeyGenerator keyGenerator;
  privateString[] keyProperties;
  privateString[] keyColumns;
  privateboolean hasNestedResultMaps;
  privateString databaseId;
  privateLog statementLog;
  privateLanguageDriver lang;
 
  privateMappedStatement() {
    // constructor disabled
  }
  ..........

我们可以看到其中的属性基本上和xml元素的属性有对应关系,其中比较重要的有表示查询参数的ParameterMap对象,表示sql查询结果映射关系的ResultMap列表resultMaps,当然最重要的还是执行动态sql计算和获取的SqlSource对象。通过这些对象的通力合作,MappedStatement接受用户的查询参数对象,动态计算出要执行的sql语句,在数据库中执行sql语句后,再将取得的数据封装为JavaBean对象返回给用户。MappedStatement对象的这些功能,也体现出了Mybatis这个框架的核心价值,“根据用户提供的查询参数对象,动态执行sql语句,并将结果封装为Java对象”。

2.org.apache.ibatis.mapping.SqlSource

SqlSource是一个接口类,在MappedStatement对象中是作为一个属性出现的,它的代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
packageorg.apache.ibatis.mapping;
 
/**
 *
 * This bean represets the content of a mapped statement read from an XML file
 * or an annotation. It creates the SQL that will be passed to the database out
 * of the input parameter received from the user.
 *
 */
publicinterface SqlSource {
 
  BoundSql getBoundSql(Object parameterObject);
 
}
SqlSource接口只有一个getBoundSql(Object parameterObject)方法,返回一个BoundSql对象。一个BoundSql对象,代表了一次sql语句的实际执行,而SqlSource对象的责任,就是根据传入的参数对象,动态计算出这个BoundSql,也就是说Mapper文件中的<if />节点的计算,是由SqlSource对象完成的。SqlSource最常用的实现类是DynamicSqlSource,来看一看它的代码:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
packageorg.apache.ibatis.scripting.xmltags;
 
importjava.util.Map;
 
importorg.apache.ibatis.builder.SqlSourceBuilder;
importorg.apache.ibatis.mapping.BoundSql;
importorg.apache.ibatis.mapping.SqlSource;
importorg.apache.ibatis.session.Configuration;
 
publicclass DynamicSqlSource implementsSqlSource {
 
  privateConfiguration configuration;
  privateSqlNode rootSqlNode;
 
  publicDynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }
 
  publicBoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = newDynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = newSqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null? Object.class: parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for(Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    returnboundSql;
  }
 
}

其中的

?
1
rootSqlNode.apply(context);

这句调用语句,启动了一个非常精密的递归实现的动态计算sql语句的过程,计算过程使用Ognl来根据传入的参数对象计算表达式,生成该次调用过程中实际执行的sql语句。

3.org.apache.ibatis.scripting.xmltags.DynamicContext

DynamicContext类中,有对传入的parameterObject对象进行“map”化处理的部分,也就是说,你传入的pojo对象,会被当作一个键值对数据来源来进行处理,读取这个pojo对象的接口,还是Map对象。从DynamicContext的源码中,能看到很明显的线索。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
importjava.util.HashMap;
importjava.util.Map;
 
importognl.OgnlException;
importognl.OgnlRuntime;
importognl.PropertyAccessor;
 
importorg.apache.ibatis.reflection.MetaObject;
importorg.apache.ibatis.session.Configuration;
 
publicclass DynamicContext {
 
  publicstatic final String PARAMETER_OBJECT_KEY = "_parameter";
  publicstatic final String DATABASE_ID_KEY = "_databaseId";
 
  static{
    OgnlRuntime.setPropertyAccessor(ContextMap.classnewContextAccessor());
  }
 
  privatefinal ContextMap bindings;
  privatefinal StringBuilder sqlBuilder = newStringBuilder();
  privateint uniqueNumber = 0;
 
  publicDynamicContext(Configuration configuration, Object parameterObject) {
    if(parameterObject != null&& !(parameterObject instanceofMap)) {
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      bindings = newContextMap(metaObject);
    else{
      bindings = newContextMap(null);
    }
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }
 
  publicMap<String, Object> getBindings() {
    returnbindings;
  }
 
  publicvoid bind(String name, Object value) {
    bindings.put(name, value);
  }
 
  publicvoid appendSql(String sql) {
    sqlBuilder.append(sql);
    sqlBuilder.append(" ");
  }
 
  publicString getSql() {
    returnsqlBuilder.toString().trim();
  }
 
  publicint getUniqueNumber() {
    returnuniqueNumber++;
  }
 
  staticclass ContextMap extendsHashMap<String, Object> {
    privatestatic final long serialVersionUID = 2977601501966151582L;
 
    privateMetaObject parameterMetaObject;
    publicContextMap(MetaObject parameterMetaObject) {
      this.parameterMetaObject = parameterMetaObject;
    }
 
    @Override
    publicObject get(Object key) {
      String strKey = (String) key;
      if(super.containsKey(strKey)) {
        returnsuper.get(strKey);
      }
 
      if(parameterMetaObject != null) {
        Object object = parameterMetaObject.getValue(strKey);
        if(object != null) {
          super.put(strKey, object);
        }
 
        returnobject;
      }
 
      returnnull;
    }
  }
 
  staticclass ContextAccessor implementsPropertyAccessor {
 
    publicObject getProperty(Map context, Object target, Object name)
        throwsOgnlException {
      Map map = (Map) target;
 
      Object result = map.get(name);
      if(result != null) {
        returnresult;
      }
 
      Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
      if(parameterObject instanceofMap) {
          return((Map)parameterObject).get(name);
      }
 
      returnnull;
    }
 
    publicvoid setProperty(Map context, Object target, Object name, Object value)
        throwsOgnlException {
      Map map = (Map) target;
      map.put(name, value);
    }
  }
}
在DynamicContext的构造函数中,可以看到,根据传入的参数对象是否为Map类型,有两个不同构造ContextMap的方式。而ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。具体来说,当传入的参数对象不是Map类型时,Mybatis会将传入的POJO对象用MetaObject对象来封装,当动态计算sql过程需要获取数据时,用Map接口的get方法包装 MetaObject对象的取值过程。


我们都知道,Mybatis中采用了Ognl来计算动态sql语句,DynamicContext类中的这个静态初始块,很好的说明了这一点

?
1
2
3
static{
  OgnlRuntime.setPropertyAccessor(ContextMap.classnewContextAccessor());
}

ContextAccessor也是DynamicContext的内部类,实现了Ognl中的PropertyAccessor接口,为Ognl提供了如何使用ContextMap参数对象的说明,这个类也为整个参数对象“map”化划上了最后一笔。

现在我们能比较清晰的描述一下Mybatis中的参数传递和使用过程了:将传入的参数对象统一封装为ContextMap对象(继承了HashMap对象),然后Ognl运行时环境在动态计算sql语句时,会按照ContextAccessor中描述的Map接口的方式来访问和读取ContextMap对象,获取计算过程中需要的参数。ContextMap对象内部可能封装了一个普通的POJO对象,也可以是直接传递的Map对象,当然从外部是看不出来的,因为都是使用Map的接口来读取数据。

结合一个例子来理解一下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
    publicvoid testSqlSource() throwsException {
        String resource = "mybatis/mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder()
                .build(inputStream);
        SqlSession session = sqlSessionFactory.openSession();
 
        try{
            Configuration configuration = session.getConfiguration();
            MappedStatement mappedStatement = configuration
                    .getMappedStatement("mybatis.UserDao.find2");
            assertNotNull(mappedStatement);
             
            UserBean param = newUserBean();
            param.setUserName("admin");
            param.setUserPassword("admin");
            BoundSql boundSql = mappedStatement.getBoundSql(param);
            String sql = boundSql.getSql();
 
            Map<String, Object> map = newHashMap<String, Object>();
            map.put("userName""admin");
            map.put("userPassword""admin");
            BoundSql boundSql2 = mappedStatement.getBoundSql(map);
            String sql2 = boundSql2.getSql();
 
            assertEquals(sql, sql2);
             
            UserBean bean = session.selectOne("mybatis.UserDao.find2", map);
            assertNotNull(bean);
 
        finally{
            session.close();
        }
 
    }

上面这个Junit测试方法,是我写的一个测试用例中的一小段,其中的UserBean对象,就是一个有三个属性userName,userPassword,createDate的POJO对象,对应的Mapper文件是文章开头给出的配置文件。

第一次测试,我使用的是一个UserBean对象,来获取和计算sql语句,而第二次我是使用了一个HashMap对象,按照属性的名字,我分别设置了两个键值对象,我甚至还直接使用它来启动了一次session对象的查询selectOne。所有这些操作,都是测试通过(绿条)。这充分说明了,Mybatis参数获取过程中,对Map对象和普通POJO对象的无差别化,因为在内部,两者都会被封装,然后通过Map接口来访问!

34、SqlSource解析

SqlSourceBuilder主要是用来构建SQLSource的,不管是动态SQL还是静态SQL,最后都是调用这个类生成静态sql字段parameterProperties支持的配置项private...
  • L_Sail
  • L_Sail
  • 2017年06月27日 23:07
  • 430

Mybatis sql解析过程

一、Mybatis解析sql的时机 Mybatis对于用户在XXMapper.xml文件中配置的sql解析主要分为2个时机 静态sql:程序启动的时候解析 动态sql:用户进行查询等sql相关操作的时...

MappedStatement,SqlSource,BoundSql

转载自http://www.open-open.com/lib/view/open1363572227609.html 本文基于Mybatis3.2.0版本的代码。 1.org...

Mybatis源码研究之DynamicContext

DynamicContext为POJO对象Map化提供了很好的借鉴,抹平了访问POJO和Map对象的差异.
  • lqzkcx3
  • lqzkcx3
  • 2017年10月18日 21:19
  • 62

MySQL导入.sql文件及常用命令

MySQL导入.sql文件及常用命令在MySQL Qurey   Brower中直接导入*.sql脚本,是不能一次执行多条sql命令的,在mysql中执行sql文件的命令:mysql> source ...

MySQL执行外部sql脚本文件的命令

sql脚本是包含一到多个sql命令的sql语句,我们可以将这些sql脚本放在一个文本文件中(我们称之为“sql脚本文件”),然后通过相关的命令执行这个sql脚本文件。基本步骤如下: 1、创建包含sq...
  • vebasan
  • vebasan
  • 2012年05月31日 15:24
  • 188370

Mybatis解析动态sql原理分析

前言 废话不多说,直接进入文章。 我们在使用mybatis的时候,会在xml中编写sql语句。 比如这段动态sql代码: "update" parameterType="org.forma...

深入浅出Mybatis-sql自动生成

本文提供了一种自动生成sql语句的方法,它针对的对象是有主键或唯一索引的单表,提供的操作有增、删、改、查4种。理解本文和本文的提供的代码需要有java注解的知识,因为本文是基于注解生成sql的。本文适...

mybatis源码分析

  • 2015年11月20日 10:07
  • 4.62MB
  • 下载

Mybatis3源码分析(21)-Mapper实现-动态代理

当定义好一个Mapper接口(UserDao)里,我们并不需要去实现这个类,但sqlSession.getMapper()最终会返回一个实现该接口的对象。这个对象是Mybatis利用jdk的动态代理实...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:MyBatis源码分析之MappedStatemenet,SqlSource,DynamicContext
举报原因:
原因补充:

(最多只允许输入30个字)