詳解MyBatis Mapper 代理實現資料庫呼叫原理
1. Mapper 代理層執行
Mapper 代理上執行方法呼叫時,呼叫被委派給 MapperProxy 來處理。
public class MapperProxy<T> implements InvocationHandler,Serializable { private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method,MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession,Class<T> mapperInterface,Map<Method,MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } public Object invoke(Object proxy,Method method,Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this,args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } // 接口裡宣告的方法,轉換為 MapperMethod 來呼叫 final MapperMethod mapperMethod = cachedMapperMethod(method); // 與 Spring 整合時此處的 sqlSession 仍然 SqlSessionTemplate return mapperMethod.execute(sqlSession,args); } private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface,method,sqlSession.getConfiguration()); methodCache.put(method,mapperMethod); } return mapperMethod; } }
MapperMethod 根據 mapperInterface.getName() + "." + method.getName() 從 Configuration 物件裡找到對應的 MappedStatement ,從而得到要執行的 SQL 操作型別(insert/delete/update/select/flush),然後呼叫傳入的 sqlSession 例項上的相應的方法。
public Object execute(SqlSession sqlSession,Object[] args) { Object result; if (SqlCommandType.INSERT == command.getType()) { // 把引數轉換為 SqlSession 能處理的格式 Object param = method.convertArgsToSqlCommandParam(args); // 在 sqlSession 上執行並處理結果 result = rowCountResult(sqlSession.insert(command.getName(),param)); } else if (SqlCommandType.UPDATE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(),param)); ...省略
如果上述方法傳入的是 SqlSessionTemplate ,那麼這些方法呼叫會被 SqlSessionInterceptor 攔截,加入與 Spring 事務管理機制協作的邏輯,具體可以看這篇文章MyBatis 事務管理,這裡不再展開,最終會呼叫到 DefaultSqlSession 例項上的方法。
2. 會話層的執行過程
SqlSession 裡宣告的所有方法的第一個引數如果是 String statement ,則都是 mapperInterface.getName() + "." + method.getName() ,表示要呼叫的 SQL 語句的識別符號。通過它從 configuration 找到 MappedStatement 。
會話層最主要的邏輯是進行引數的包裝,獲取對應的 MappedStatement ,然後呼叫持有的 Executor 的方法去執行。
public class DefaultSqlSession implements SqlSession { private Configuration configuration; private Executor executor; private boolean autoCommit; private boolean dirty; private List<Cursor<?>> cursorList; public DefaultSqlSession(Configuration configuration,Executor executor,boolean autoCommit) { this.configuration = configuration; this.executor = executor; this.dirty = false; this.autoCommit = autoCommit; } 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(); } }
3. Executor 執行的過程
我們知道 JDBC 裡有三種資料庫語句: java.sql.Statement/PreparedStatement/CallableStatement ,每種語句的執行方式是不一樣的,MyBatis 建立了 StatementHandler 抽象來表示資料庫語句的處理邏輯,有對應的三種具體實現: SimpleStatementHandler/PreparedStatementHandler/CallableStatementHandler 。
RoutingStatementHandler 是個門面模式,構建時根據要執行的資料庫語句型別例項化 SimpleStatementHandler/PreparedStatementHandler/CallableStatementHandler 中的一個類作為目標 delegate,並把呼叫都轉給這個 delegate 的方法。
不同的 handler 實現實現了對應的:資料庫語句的建立、引數化設定、執行語句。
通過這層抽象,MyBatis 統一了 Executor 裡的執行流程,以下以 SimpleExecutor 的流程為例:
1. 對於傳入的 MappedStatement ms ,得到 Configuration configuration 。
2. configuration 通過 ms 的語句型別得到一個 RoutingStatementHandler 的例項(內部有個 delegate 可以委派) handler ,並用 InterceptorChain 對 handler 例項進行裝飾。
3. 通過 SimpleExecutor 持有的 Transaction 例項獲取對應的資料庫連線 connection。
4. handler 通過資料庫連線初始化資料庫語句 java.sql.Statement 或其子類 stmt ,設定超時時間和 fetchSize 。
5. 用 handler 對 stmt 進行引數化處理(比如 PreparedStatement 設定預編譯語句的引數值)。
6. handler 執行相應的 stmt 完成資料庫操作。
7. 用 ResultSetHandler 對結果集進行處理。 ResultSetHandler 會呼叫 TypeHandler 來進行 Java 型別與資料庫列型別之間轉換。
// SimpleExecutor public <E> List<E> doQuery(MappedStatement ms,RowBounds rowBounds,ResultHandler resultHandler,BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // 建立 handler 來負責具體的執行 StatementHandler handler = configuration.newStatementHandler(wrapper,ms,parameter,resultHandler,boundSql); // 建立資料庫語句 stmt = prepareStatement(handler,ms.getStatementLog()); // 執行資料庫操作 return handler.<E>query(stmt,resultHandler); } finally { closeStatement(stmt); } } // Configuration public StatementHandler newStatementHandler(Executor executor,MappedStatement mappedStatement,Object parameterObject,BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor,mappedStatement,parameterObject,boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } // RoutingStatementHandler public RoutingStatementHandler(Executor executor,MappedStatement ms,BoundSql boundSql) { // 根據SQL語句的執行方式建立對應的 handler 例項 switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor,boundSql); break; case PREPARED: delegate = new PreparedStatementHandler(executor,boundSql); break; case CALLABLE: delegate = new CallableStatementHandler(executor,boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } } private Statement prepareStatement(StatementHandler handler,Log statementLog) throws SQLException { // 建立資料庫連線 Connection connection = getConnection(statementLog); // 建立資料庫語句 Statement stmt = handler.prepare(connection,transaction.getTimeout()); // 引數化設定 handler.parameterize(stmt); return stmt; } protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = transaction.getConnection(); if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection,statementLog,queryStack); } else { return connection; } } // BaseStatementHandler public Statement prepare(Connection connection,Integer transactionTimeout) throws SQLException { ErrorContext.instance().sql(boundSql.getSql()); Statement statement = null; try { // 由具體的子類來建立對應的 Statement 例項 statement = instantiateStatement(connection); // 通用引數設定 setStatementTimeout(statement,transactionTimeout); setFetchSize(statement); return statement; } catch (SQLException e) { closeStatement(statement); throw e; } catch (Exception e) { closeStatement(statement); throw new ExecutorException("Error preparing statement. Cause: " + e,e); } } // PreparedStatementHandler protected Statement instantiateStatement(Connection connection) throws SQLException { String sql = boundSql.getSql(); if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { String[] keyColumnNames = mappedStatement.getKeyColumns(); if (keyColumnNames == null) { return connection.prepareStatement(sql,PreparedStatement.RETURN_GENERATED_KEYS); } else { return connection.prepareStatement(sql,keyColumnNames); } } else if (mappedStatement.getResultSetType() != null) { return connection.prepareStatement(sql,mappedStatement.getResultSetType().getValue(),ResultSet.CONCUR_READ_ONLY); } else { return connection.prepareStatement(sql); } } // PreparedStatementHandler public void parameterize(Statement statement) throws SQLException { parameterHandler.setParameters((PreparedStatement) statement); }
4. 問題
只在 XML 裡定義 SQL、沒有對應的 Java 介面類能否使用 MyBatis ?
答:可以,通過 SqlSession 的方法來呼叫 XML 裡的 SQL 語句。
Mapper 介面類裡可以進行方法過載嗎?
答:不能,因為 MyBatis 里根據 類名 + “.” + 方法名 來查詢 SQL 語句,過載會導致這樣的組合出現多條結果。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。