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接口來訪問!