1. 程式人生 > 程式設計 >詳解MyBatis Mapper 代理實現資料庫呼叫原理

詳解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 語句,過載會導致這樣的組合出現多條結果。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。