MyBatis框架及原理分析
MyBatis 是支援定製化 SQL、儲存過程以及高階對映的優秀的持久層框架,其主要就完成2件事情:
- 封裝JDBC操作
- 利用反射打通Java類與SQL語句之間的相互轉換
MyBatis的主要設計目的就是讓我們對執行SQL語句時對輸入輸出的資料管理更加方便,所以方便地寫出SQL和方便地獲取SQL的執行結果才是MyBatis的核心競爭力。
MyBatis的配置
MyBatis框架和其他絕大部分框架一樣,需要一個配置檔案,其配置檔案大致如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="false"/> <!--<setting name="logImpl" value="STDOUT_LOGGING"/> <!– 列印日誌資訊 –>--> </settings> <typeAliases> <typeAlias type="com.luo.dao.UserDao" alias="User"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <!--事務管理型別--> <dataSource type="POOLED"> <property name="username" value="luoxn28"/> <property name="password" value="123456"/> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://192.168.1.150/ssh_study"/> </dataSource> </environment> </environments> <mappers> <mapper resource="userMapper.xml"/> </mappers> </configuration>
以上配置中,最重要的是資料庫引數的配置,比如使用者名稱密碼等,如果配置了資料表對應的mapper檔案,則需要將其加入到<mappers>節點下。
MyBatis的主要成員
- Configuration MyBatis所有的配置資訊都儲存在Configuration物件之中,配置檔案中的大部分配置都會儲存到該類中
- SqlSession 作為MyBatis工作的主要頂層API,表示和資料庫互動時的會話,完成必要資料庫增刪改查功能
- Executor MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護
- StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數等
- ParameterHandler 負責對使用者傳遞的引數轉換成JDBC Statement 所對應的資料型別
- ResultSetHandler 負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合
- TypeHandler 負責java資料型別和jdbc資料型別(也可以說是資料表列型別)之間的對映和轉換
- MappedStatement MappedStatement維護一條<select|update|delete|insert>節點的封裝
- SqlSource 負責根據使用者傳遞的parameterObject,動態地生成SQL語句,將資訊封裝到BoundSql物件中,並返回
- BoundSql 表示動態生成的SQL語句以及相應的引數資訊
以上主要成員在一次資料庫操作中基本都會涉及,在SQL操作中重點需要關注的是SQL引數什麼時候被設定和結果集怎麼轉換為JavaBean物件的,這兩個過程正好對應StatementHandler和ResultSetHandler類中的處理邏輯。
(圖片來自《深入理解mybatis原理》 MyBatis的架構設計以及例項分析)
MyBatis的初始化
MyBatis的初始化的過程其實就是解析配置檔案和初始化Configuration的過程,MyBatis的初始化過程可用以下幾行程式碼來表述:
String resource = "mybatis.xml"; // 載入mybatis的配置檔案(它也載入關聯的對映檔案) InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(resource); } catch (IOException e) { e.printStackTrace(); } // 構建sqlSession的工廠 sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
首先會建立SqlSessionFactory建造者物件,然後由它進行建立SqlSessionFactory。這裡用到的是建造者模式,建造者模式最簡單的理解就是不手動new物件,而是由其他類來進行物件的建立。
// SqlSessionFactoryBuilder類 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); // 開始進行解析了 :) } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
XMLConfigBuilder物件會進行XML配置檔案的解析,實際為configuration節點的解析操作。
// XMLConfigBuilder類 public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; } private void parseConfiguration(XNode root) { try { //issue #117 read properties first propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 /* 處理environments節點資料 */ environmentsElement(root.evalNode("environments")); 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); } }
在configuration節點下會依次解析properties/settings/.../mappers等節點配置。在解析environments節點時,會根據transactionManager的配置來建立事務管理器,根據dataSource的配置來建立DataSource物件,這裡麵包含了資料庫登入的相關資訊。在解析mappers節點時,會讀取該節點下所有的mapper檔案,然後進行解析,並將解析後的結果存到configuration物件中。
// XMLConfigBuilder類 private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); if (isSpecifiedEnvironment(id)) { /* 建立事務管理器 */ TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); DataSource dataSource = dsFactory.getDataSource(); /* 建造者模式 設計模式 */ Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } } // 解析單獨的mapper檔案 private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); // 開始解析mapper檔案了 :) } 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."); } } } } }
解析完MyBatis配置檔案後,configuration就初始化完成了,然後根據configuration物件來建立SqlSession,到這裡時,MyBatis的初始化的征程已經走完了。
// SqlSessionFactoryBuilder類 public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
MyBatis的SQL查詢流程
SQL語句的執行才是MyBatis的重要職責,該過程就是通過封裝JDBC進行操作,然後使用Java反射技術完成JavaBean物件到資料庫引數之間的相互轉換,這種對映關係就是有TypeHandler物件來完成的,在獲取資料表對應的元資料時,會儲存該表所有列的資料庫型別,大致邏輯如下所示:
/* Get resultSet metadata */ ResultSetMetaData metaData = resultSet.getMetaData(); int column = metaData.getColumnCount(); for (int i = 1; i <= column; i++) { JdbcType jdbcType = JdbcType.forCode(metaData.getColumnType(i)); typeHandlers.add(TypeHandlerRegistry.getTypeHandler(jdbcType)); columnNames.add(metaData.getColumnName(i)); }
使用如下程式碼進行SQL查詢操作:
sqlSession = sessionFactory.openSession(); User user = sqlSession.selectOne("com.luo.dao.UserDao.getUserById", 1); System.out.println(user);
建立sqlSession的過程其實就是根據configuration中的配置來建立對應的類,然後返回建立的sqlSession物件。呼叫selectOne方法進行SQL查詢,selectOne方法最後呼叫的是selectList,在selectList中,會查詢configuration中儲存的MappedStatement物件,mapper檔案中一個sql語句的配置對應一個MappedStatement物件,然後呼叫執行器進行查詢操作。
// DefaultSqlSession類 public <T> T selectOne(String statement, Object parameter) { // Popular vote was to return null on 0 results and throw exception on too many. List<T> list = this.<T>selectList(statement, parameter); if (list.size() == 1) { return list.get(0); } else if (list.size() > 1) { throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else { return null; } } public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); 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(); } }
執行器在query操作中,優先會查詢快取是否命中,命中則直接返回,否則從資料庫中查詢。
// CachingExecutor類 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { /* 獲取關聯引數的sql,boundSql */ BoundSql boundSql = ms.getBoundSql(parameterObject); /* 建立cache key值 */ CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { /* 獲取二級快取例項 */ Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; /** * 先往localCache中插入一個佔位物件,這個地方 */ localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } /* 往快取中寫入資料,也就是快取查詢結果 */ localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
真正的doQuery操作是由SimplyExecutor代理來完成的,該方法中有2個子流程,一個是SQL引數的設定,另一個是SQL查詢操作和結果集的封裝。
// 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); /* 子流程1: SQL查詢引數的設定 */ stmt = prepareStatement(handler, ms.getStatementLog()); /* 子流程2: SQL查詢操作和結果集封裝 */ return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } }
子流程1 SQL查詢引數的設定:
首先獲取資料庫connection連線,然後準備statement,然後就設定SQL查詢中的引數值。開啟一個connection連線,在使用完後不會close,而是儲存下來,當下次需要開啟連線時就直接返回。
// SimpleExecutor類 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; /* 獲取Connection連線 */ Connection connection = getConnection(statementLog); /* 準備Statement */ stmt = handler.prepare(connection, transaction.getTimeout()); /* 設定SQL查詢中的引數值 */ handler.parameterize(stmt); return stmt; } // DefaultParameterHandler類 public void setParameters(PreparedStatement ps) { /** * 設定SQL引數值,從ParameterMapping中讀取引數值和型別,然後設定到SQL語句中 */ ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { 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)) { // issue #448 ask first for additional params value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { 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 { 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); } } } } }
子流程2 SQL查詢結果集的封裝:
// SimpleExecutor類 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // 執行查詢操作 ps.execute(); // 執行結果集封裝 return resultSetHandler.<E> handleResultSets(ps); } // DefaultReseltSetHandler類 public List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); final List<Object> multipleResults = new ArrayList<Object>(); int resultSetCount = 0; /** * 獲取第一個ResultSet,同時獲取資料庫的MetaData資料,包括資料表列名、列的型別、類序號等。 * 這些資訊都儲存在了ResultSetWrapper中了 */ ResultSetWrapper rsw = getFirstResultSet(stmt); List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } } return collapseSingleResultList(multipleResults); }
ResultSetWrapper是ResultSet的包裝類,呼叫getFirstResultSet方法獲取第一個ResultSet,同時獲取資料庫的MetaData資料,包括資料表列名、列的型別、類序號等,這些資訊都儲存在ResultSetWrapper類中了。然後呼叫handleResultSet方法來來進行結果集的封裝。
// DefaultResultSetHandler類 private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException { try { if (parentMapping != null) { handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping); } else { if (resultHandler == null) { DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory); handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null); multipleResults.add(defaultResultHandler.getResultList()); } else { handleRowValues(rsw, resultMap, resultHandler, rowBounds, null); } } } finally { // issue #228 (close resultsets) closeResultSet(rsw.getResultSet()); } }
這裡呼叫handleRowValues方法進行結果值的設定。
// DefaultResultSetHandler類 public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { if (resultMap.hasNestedResultMaps()) { ensureNoRowBounds(); checkResultHandler(); handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } else { // 封裝資料 handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } } private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>(); skipRows(rsw.getResultSet(), rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) { ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null); Object rowValue = getRowValue(rsw, discriminatedResultMap); storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); } } private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); // createResultObject為新建立的物件,資料表對應的類 Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null); if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { final MetaObject metaObject = configuration.newMetaObject(rowValue); boolean foundValues = this.useConstructorMappings; if (shouldApplyAutomaticMappings(resultMap, false)) { // 這裡把資料填充進去,metaObject中包含了resultObject資訊 foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues; } foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues; foundValues = lazyLoader.size() > 0 || foundValues; rowValue = (foundValues || configuration.isReturnInstanceForEmptyRow()) ? rowValue : null; } return rowValue; } private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix); boolean foundValues = false; if (autoMapping.size() > 0) { // 這裡進行for迴圈呼叫,因為user表中總共有7列,所以也就呼叫7次 for (UnMappedColumnAutoMapping mapping : autoMapping) { // 這裡將esultSet中查詢結果轉換為對應的實際型別 final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column); if (value != null) { foundValues = true; } if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) { // gcode issue #377, call setter on nulls (value is not 'found') metaObject.setValue(mapping.property, value); } } } return foundValues; }
mapping.typeHandler.getResult會獲取查詢結果值的實際型別,比如我們user表中id欄位為int型別,那麼它就對應Java中的Integer型別,然後通過呼叫statement.getInt("id")來獲取其int值,其型別為Integer。metaObject.setValue方法會把獲取到的Integer值設定到Java類中的對應欄位。
// MetaObject類 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); } }
metaValue.setValue方法最後會呼叫到Java類中對應資料域的set方法,這樣也就完成了SQL查詢結果集的Java類封裝過程。最後貼一張呼叫棧到達Java類的set方法中的快照:
MyBatis快取
MyBatis提供查詢快取,用於減輕資料庫壓力,提高效能。MyBatis提供了一級快取和二級快取。
一級快取是SqlSession級別的快取,每個SqlSession物件都有一個雜湊表用於快取資料,不同SqlSession物件之間快取不共享。同一個SqlSession物件物件執行2遍相同的SQL查詢,在第一次查詢執行完畢後將結果快取起來,這樣第二遍查詢就不用向資料庫查詢了,直接返回快取結果即可。MyBatis預設是開啟一級快取的。
二級快取是mapper級別的快取,二級快取是跨SqlSession的,多個SqlSession物件可以共享同一個二級快取。不同的SqlSession物件執行兩次相同的SQL語句,第一次會將查詢結果進行快取,第二次查詢直接返回二級快取中的結果即可。MyBatis預設是不開啟二級快取的,可以在配置檔案中使用如下配置來開啟二級快取:
<settings> <setting name="cacheEnabled" value="true"/> </settings>
當SQL語句進行更新操作(刪除/新增/更新)時,會清空對應的快取,保證快取中儲存的都是最新的資料。MyBatis的二級快取對細粒度的資料級別的快取實現不友好,比如如下需求:對商品資訊進行快取,由於商品資訊查詢訪問量大,但是要求使用者每次都能查詢最新的商品資訊,此時如果使用mybatis的二級快取就無法實現當一個商品變化時只重新整理該商品的快取資訊而不重新整理其它商品的資訊,因為mybaits的二級快取區域以mapper為單位劃分,當一個商品資訊變化會將所有商品資訊的快取資料全部清空。解決此類問題需要在業務層根據需求對資料有針對性快取,具體業務具體實現。