1. 程式人生 > >Mybatis中幾個重要類

Mybatis中幾個重要類

本文基於Mybatis3.2.0版本的程式碼。

1.org.apache.ibatis.mapping.MappedStatement

MappedStatement類在Mybatis框架中用於表示XML檔案中一個sql語句節點,即一個<select />、<update />或者<insert />標籤。Mybatis框架在初始化階段會對XML配置檔案進行讀取,將其中的sql語句節點物件化為一個個MappedStatement物件。比如下面這個非常簡單的XML mapper檔案:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mybatis.UserDao">

	<cache type="org.mybatis.caches.ehcache.LoggingEhcache" />

	<resultMap id="userResultMap" type="UserBean">
		<id property="userId" column="user_id" />
		<result property="userName" column="user_name" />
		<result property="userPassword" column="user_password" />
		<result property="createDate" column="create_date" />
	</resultMap>

	<select id="find" parameterType="UserBean" resultMap="userResultMap">
		select * from user
		<where>
			<if test="userName!=null and userName!=''">
				and user_name = #{userName}
			</if>
			<if test="userPassword!=null and userPassword!=''">
				and user_password = #{userPassword}
			</if>
			<if test="createDate !=null">
				and create_date = #{createDate}
			</if>
		</where>
	</select>

	<!-- 說明mybatis中的sql語句節點和對映的介面中的方法,並不是一一對應的關係,而是獨立的,可以取任意不重複的名稱 -->
	<select id="find2" parameterType="UserBean" resultMap="userResultMap">
		select * from user
		<where>
			<if test="userName!=null and userName!=''">
				and user_name = #{userName}
			</if>
			<if test="userPassword!=null and userPassword!=''">
				and user_password = #{userPassword}
			</if>
			<if test="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物件的原始碼,看一下其中的私有屬性。

public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;

  private MappedStatement() {
    // 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物件中是作為一個屬性出現的,它的程式碼如下:

package org.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.
 * 
 */
public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}
SqlSource介面只有一個getBoundSql(Object parameterObject)方法,返回一個BoundSql物件。一個BoundSql物件,代表了一次sql語句的實際執行,而SqlSource物件的責任,就是根據傳入的引數物件,動態計算出這個BoundSql,也就是說Mapper檔案中的<if />節點的計算,是由SqlSource物件完成的。SqlSource最常用的實現類是DynamicSqlSource,來看一看它的程式碼:
package org.apache.ibatis.scripting.xmltags;

import java.util.Map;

import org.apache.ibatis.builder.SqlSourceBuilder;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.session.Configuration;

public class DynamicSqlSource implements SqlSource {

  private Configuration configuration;
  private SqlNode rootSqlNode;

  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }

  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(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());
    }
    return boundSql;
  }

}


其中的

rootSqlNode.apply(context);
這句呼叫語句,啟動了一個非常精密的遞迴實現的動態計算sql語句的過程,計算過程使用Ognl來根據傳入的引數物件計算表示式,生成該次呼叫過程中實際執行的sql語句。

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

DynamicContext類中,有對傳入的parameterObject物件進行“map”化處理的部分,也就是說,你傳入的pojo物件,會被當作一個鍵值對資料來源來進行處理,讀取這個pojo物件的介面,還是Map物件。從DynamicContext的原始碼中,能看到很明顯的線索。

import java.util.HashMap;
import java.util.Map;

import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.PropertyAccessor;

import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;

public class DynamicContext {

  public static final String PARAMETER_OBJECT_KEY = "_parameter";
  public static final String DATABASE_ID_KEY = "_databaseId";

  static {
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
  }

  private final ContextMap bindings;
  private final StringBuilder sqlBuilder = new StringBuilder();
  private int uniqueNumber = 0;

  public DynamicContext(Configuration configuration, Object parameterObject) {
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      bindings = new ContextMap(metaObject);
    } else {
      bindings = new ContextMap(null);
    }
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

  public Map<String, Object> getBindings() {
    return bindings;
  }

  public void bind(String name, Object value) {
    bindings.put(name, value);
  }

  public void appendSql(String sql) {
    sqlBuilder.append(sql);
    sqlBuilder.append(" ");
  }

  public String getSql() {
    return sqlBuilder.toString().trim();
  }

  public int getUniqueNumber() {
    return uniqueNumber++;
  }

  static class ContextMap extends HashMap<String, Object> {
    private static final long serialVersionUID = 2977601501966151582L;

    private MetaObject parameterMetaObject;
    public ContextMap(MetaObject parameterMetaObject) {
      this.parameterMetaObject = parameterMetaObject;
    }

    @Override
    public Object get(Object key) {
      String strKey = (String) key;
      if (super.containsKey(strKey)) {
        return super.get(strKey);
      }

      if (parameterMetaObject != null) {
        Object object = parameterMetaObject.getValue(strKey);
        if (object != null) {
          super.put(strKey, object);
        }

        return object;
      }

      return null;
    }
  }

  static class ContextAccessor implements PropertyAccessor {

    public Object getProperty(Map context, Object target, Object name)
        throws OgnlException {
      Map map = (Map) target;

      Object result = map.get(name);
      if (result != null) {
        return result;
      }

      Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
      if (parameterObject instanceof Map) {
    	  return ((Map)parameterObject).get(name);
      }

      return null;
    }

    public void setProperty(Map context, Object target, Object name, Object value)
        throws OgnlException {
      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類中的這個靜態初始塊,很好的說明了這一點

static {
    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的介面來讀取資料。

結合一個例子來理解一下:

@Test
	public void testSqlSource() throws Exception {
		String resource = "mybatis/mybatis-config.xml";
		InputStream inputStream = Resources.getResourceAsStream(resource);
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
				.build(inputStream);
		SqlSession session = sqlSessionFactory.openSession();

		try {
			Configuration configuration = session.getConfiguration();
			MappedStatement mappedStatement = configuration
					.getMappedStatement("mybatis.UserDao.find2");
			assertNotNull(mappedStatement);
			
			UserBean param = new UserBean();
			param.setUserName("admin");
			param.setUserPassword("admin");
			BoundSql boundSql = mappedStatement.getBoundSql(param);
			String sql = boundSql.getSql();

			Map<String, Object> map = new HashMap<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介面來訪問!