【MyBatis原始碼分析】insert方法、update方法、delete方法處理流程(上篇)

五月的倉頡發表於2017-05-09

開啟一個會話Session

前文分析了MyBatis將配置檔案轉換為Java物件的流程,本文開始分析一下insert方法、update方法、delete方法處理的流程,至於為什麼這三個方法要放在一起說,是因為:

  1. 從語義的角度,insert、update、delete都是屬於對資料庫的行進行更新操作
  2. 從實現的角度,我們熟悉的PreparedStatement裡面提供了兩種execute方法,一種是executeUpdate(),一種是executeQuery(),前者對應的是insert、update與delete,後者對應的是select,因此對於MyBatis來說只有update與select

示例程式碼為這段:

 1 public long insertMail(Mail mail) {
 2     SqlSession ss = ssf.openSession();
 3     try {
 4         int rows = ss.insert(NAME_SPACE + "insertMail", mail);
 5         ss.commit();
 6         if (rows > 0) {
 7             return mail.getId();
 8         }
 9         return 0;
10     } catch (Exception e) {
11         ss.rollback();
12         return 0;
13     } finally {
14         ss.close();
15     }
16 }

首先關注的是第2行的程式碼,ssf是SqlSessionFactory,其型別是DefaultSqlSessionFactory,上文最後已經分析過了,這裡通過DefaultSqlSessionFactory來開啟一個Session,通過Session去進行CRUD操作。

看一下openSession()方法的實現:

1 public SqlSession openSession() {
2     return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
3 }

顧名思義,從DataSource中獲取Session,第一個引數的值是ExecutorType.SIMPLE,繼續跟程式碼:

 1 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
 2     Transaction tx = null;
 3     try {
 4       final Environment environment = configuration.getEnvironment();
 5       final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
 6       tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
 7       final Executor executor = configuration.newExecutor(tx, execType);
 8       return new DefaultSqlSession(configuration, executor, autoCommit);
 9     } catch (Exception e) {
10       closeTransaction(tx); // may have fetched a connection so lets call close()
11       throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
12     } finally {
13       ErrorContext.instance().reset();
14     }
15 }

第4行的程式碼,獲取配置的環境資訊Environment。

第5行的程式碼,從Environment中獲取事物工廠TransactionFactory,由於<environment>中配置的是"JDBC",因此其真實型別是JdbcTransactionFactory,上文有說過。

第6行的程式碼,根據Environment中的DataSource(其實際型別是PooledDataSource)、TransactionIsolationLevel、autoCommit三個引數從TransactionFactory中獲取一個事物,注意第三個引數autoCommit,它是openSession()方法中傳過來的,其值為false,即MyBatis預設事物是不自動提交的

第7行的程式碼,實現跟一下:

 1 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
 2     executorType = executorType == null ? defaultExecutorType : executorType;
 3     executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
 4     Executor executor;
 5     if (ExecutorType.BATCH == executorType) {
 6       executor = new BatchExecutor(this, transaction);
 7     } else if (ExecutorType.REUSE == executorType) {
 8       executor = new ReuseExecutor(this, transaction);
 9     } else {
10       executor = new SimpleExecutor(this, transaction);
11     }
12     if (cacheEnabled) {
13       executor = new CachingExecutor(executor);
14     }
15     executor = (Executor) interceptorChain.pluginAll(executor);
16     return executor;
17 }

這裡總結一下:

  • 根據ExecutorType獲取一個執行器,這裡是第10行的SimpleExecutor
  • 如果滿足第12行的判斷開啟快取功能,則執行第13行的程式碼。第13行的程式碼使用到了裝飾器模式,傳入Executor,給SimpleExecutor裝飾上了快取功能
  • 第15行的程式碼用於設定外掛

這樣就獲取了一個Executor。最後將Executor、Configuration、autoCommit三個變數作為引數,例項化一個SqlSession出來,其實際型別為DefaultSqlSession。

 

insert方法執行流程

在看了openSession()方法知道最終獲得了一個DefaultSqlSession之後,看一下DefaultSqlSession的insert方法是如何實現的:

 1 public int insert(String statement, Object parameter) {
 2     return update(statement, parameter);
 3 }

看到雖然呼叫的是insert方法,但是最終統一都會去執行update方法,delete方法也是如此,這個開頭已經說過了,這裡證明了這一點。

接著繼續看第2行的方法實現:

 1 public int update(String statement, Object parameter) {
 2     try {
 3       dirty = true;
 4       MappedStatement ms = configuration.getMappedStatement(statement);
 5       return executor.update(ms, wrapCollection(parameter));
 6     } catch (Exception e) {
 7       throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
 8     } finally {
 9       ErrorContext.instance().reset();
10     }
11 }

第4行的程式碼根據statement從Configuration中獲取MappedStatement,MappedStatement上文已經分析過了,儲存在Configuration的mappedStatements欄位中。

第5行的程式碼分為兩部分,首先wrapCollection,顧名思義包裝集合類,原始碼為:

 1 private Object wrapCollection(final Object object) {
 2     if (object instanceof Collection) {
 3       StrictMap<Object> map = new StrictMap<Object>();
 4       map.put("collection", object);
 5       if (object instanceof List) {
 6         map.put("list", object);
 7       }
 8       return map;
 9     } else if (object != null && object.getClass().isArray()) {
10       StrictMap<Object> map = new StrictMap<Object>();
11       map.put("array", object);
12       return map;
13     }
14     return object;
15 }

這裡做了三層處理:

  • 如果引數是Collection(即集合)型別,放一個key為"collection"、value為引數的鍵值對
  • 如果引數是List型別,放一個key為"list"、value為引數的鍵值對
  • 如果引數是陣列型別,放一個key為"array"、value為引數的鍵值對

將集合進行包裝之後,就可以執行Executor的update方法了,Executor上面說了,是使用裝飾器模式將SimpleExecutor加上了快取功能的CacheExecutor,它的update方法實現為:

1 public int update(MappedStatement ms, Object parameterObject) throws SQLException {
2     flushCacheIfRequired(ms);
3     return delegate.update(ms, parameterObject);
4 }

第2行的程式碼是判斷是否要求清快取的,這裡首先我們的示例配置檔案mail.xml中沒有配置<cache>,其次<insert>、<delete>、<update>、<select>中沒有配置flushCache="true"屬性,因此這一句程式碼不會執行任何操作。

第3行的程式碼delegate就是SimpleExecutor本身,因為是裝飾器模式,因此會持有介面的引用,deletegate其型別就是Executor。繼續跟程式碼,看一下update方法:

1 public int update(MappedStatement ms, Object parameter) throws SQLException {
2     ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
3     if (closed) {
4       throw new ExecutorException("Executor was closed.");
5     }
6     clearLocalCache();
7     return doUpdate(ms, parameter);
8 }

前面的沒什麼好看的,繼續跟第7行的程式碼:

 1 public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
 2     Statement stmt = null;
 3     try {
 4       Configuration configuration = ms.getConfiguration();
 5       StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
 6       stmt = prepareStatement(handler, ms.getStatementLog());
 7       return handler.update(stmt);
 8     } finally {
 9       closeStatement(stmt);
10     }
11 }

第4行的程式碼獲取MappedStatement中的Configuration物件。

第5行的程式碼獲取Statement處理器StatementHandler介面實現類,Statement是Java原生的為JDBC設計的宣告,StatementHandler介面實現類的真實型別為RoutingStatementHandler。

第6行和第7行的程式碼後文逐步分析,因為裡面一點一點封裝了我們平時寫JDBC時的一些基本步驟,比如獲取Connection,構建PreparedStatement、對execute後的結果進行處理等,先看一下prepareStatement的原始碼:

1 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
2     Statement stmt;
3     Connection connection = getConnection(statementLog);
4     stmt = handler.prepare(connection, transaction.getTimeout());
5     handler.parameterize(stmt);
6     return stmt;
7 }

後面逐步分析。

 

獲取Connection

第一步,看下獲取Connection的步驟。看一下上面getConnection方法如何實現:

1 protected Connection getConnection(Log statementLog) throws SQLException {
2     Connection connection = transaction.getConnection();
3     if (statementLog.isDebugEnabled()) {
4       return ConnectionLogger.newInstance(connection, statementLog, queryStack);
5     } else {
6       return connection;
7     }
8 }

Connection從Transaction中獲取,配置的是JDBC,這裡程式碼進入JdbcTransaction的getConnection():

1 protected Connection getConnection(Log statementLog) throws SQLException {
2     Connection connection = transaction.getConnection();
3     if (statementLog.isDebugEnabled()) {
4       return ConnectionLogger.newInstance(connection, statementLog, queryStack);
5     } else {
6       return connection;
7     }
8 }

先看一下第3行~第7行的程式碼,判斷的意思是是否開啟Statement的表示式,如果開啟,那麼第4行會給生成的Connection加上一個代理,代理的內容是在呼叫prepareStatement、prepareCall等方法前或者方法後列印日誌,具體可見ConnectionLogger、PreparedStatementLogger、ResultSetLogger與StatementLogger的invoke方法。

接著繼續跟第2行的程式碼:

1 public Connection getConnection() throws SQLException {
2     if (connection == null) {
3       openConnection();
4     }
5     return connection;
6 }

跟一下第3行的程式碼:

 1 protected void openConnection() throws SQLException {
 2     if (log.isDebugEnabled()) {
 3       log.debug("Opening JDBC Connection");
 4     }
 5     connection = dataSource.getConnection();
 6     if (level != null) {
 7       connection.setTransactionIsolation(level.getLevel());
 8     }
 9     setDesiredAutoCommit(autoCommmit);
10 }

第6行~第8行的程式碼用於設定事物隔離級別,第9行的程式碼用於設定是否自動提交事物。下面跟一下第5行的程式碼getConnection()方法:

1 public Connection getConnection() throws SQLException {
2     return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
3 }

這裡簡單提一下,在方法名中如果看到了"pop"、"push"字樣,一定要把該方法使用的資料結構和棧聯想起來,棧(stack)是一個後進先出的資料結構,"pop"、"push"是棧特有的操作,前者將棧頂的資料推送出棧讓呼叫者獲取到,後者將資料壓入棧頂

後面的getProxyConnection()方法就是將獲取到的Connection返回而已,沒什麼特殊的操作,這裡跟一下popConnection方法實現,它位於PooledDataSource類中,這是由<dataSource>標籤中的type屬性決定的:

  1 private PooledConnection popConnection(String username, String password) throws SQLException {
  2     boolean countedWait = false;
  3     PooledConnection conn = null;
  4     long t = System.currentTimeMillis();
  5     int localBadConnectionCount = 0;
  6 
  7     while (conn == null) {
  8       synchronized (state) {
  9         if (!state.idleConnections.isEmpty()) {
 10           // Pool has available connection
 11           conn = state.idleConnections.remove(0);
 12           if (log.isDebugEnabled()) {
 13             log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
 14           }
 15         } else {
 16           // Pool does not have available connection
 17           if (state.activeConnections.size() < poolMaximumActiveConnections) {
 18             // Can create new connection
 19             conn = new PooledConnection(dataSource.getConnection(), this);
 20             if (log.isDebugEnabled()) {
 21               log.debug("Created connection " + conn.getRealHashCode() + ".");
 22             }
 23           } else {
 24             // Cannot create new connection
 25             PooledConnection oldestActiveConnection = state.activeConnections.get(0);
 26             long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
 27             if (longestCheckoutTime > poolMaximumCheckoutTime) {
 28               // Can claim overdue connection
 29               state.claimedOverdueConnectionCount++;
 30               state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
 31               state.accumulatedCheckoutTime += longestCheckoutTime;
 32               state.activeConnections.remove(oldestActiveConnection);
 33               if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
 34                 try {
 35                   oldestActiveConnection.getRealConnection().rollback();
 36                 } catch (SQLException e) {
 37                   log.debug("Bad connection. Could not roll back");
 38                 }  
 39               }
 40               conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
 41               oldestActiveConnection.invalidate();
 42               if (log.isDebugEnabled()) {
 43                 log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
 44               }
 45             } else {
 46               // Must wait
 47               try {
 48                 if (!countedWait) {
 49                   state.hadToWaitCount++;
 50                   countedWait = true;
 51                 }
 52                 if (log.isDebugEnabled()) {
 53                   log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
 54                 }
 55                 long wt = System.currentTimeMillis();
 56                 state.wait(poolTimeToWait);
 57                 state.accumulatedWaitTime += System.currentTimeMillis() - wt;
 58               } catch (InterruptedException e) {
 59                 break;
 60               }
 61             }
 62           }
 63         }
 64         if (conn != null) {
 65           if (conn.isValid()) {
 66             if (!conn.getRealConnection().getAutoCommit()) {
 67               conn.getRealConnection().rollback();
 68             }
 69             conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
 70             conn.setCheckoutTimestamp(System.currentTimeMillis());
 71             conn.setLastUsedTimestamp(System.currentTimeMillis());
 72             state.activeConnections.add(conn);
 73             state.requestCount++;
 74             state.accumulatedRequestTime += System.currentTimeMillis() - t;
 75           } else {
 76             if (log.isDebugEnabled()) {
 77               log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
 78             }
 79             state.badConnectionCount++;
 80             localBadConnectionCount++;
 81             conn = null;
 82             if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
 83               if (log.isDebugEnabled()) {
 84                 log.debug("PooledDataSource: Could not get a good connection to the database.");
 85               }
 86               throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
 87             }
 88           }
 89         }
 90       }
 91 
 92     }
 93 
 94     if (conn == null) {
 95       if (log.isDebugEnabled()) {
 96         log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
 97       }
 98       throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
 99     }
100 
101     return conn;
102 }

這段方法很長,分解一下。

首先是第9行~第15行的判斷,假使空閒的Connection列表不是空的,Connection就是空閒Connection列表的第一個Connection,且移除空閒Connection列表的第一個Connection,這也符合PooledDataSource的定義,有一個Connection池,對Connection進行復用而不是每次都new出來,這就是典型的棧的操作。但是這裡有一點我認為MyBatis寫得不是很好,List的實際型別是ArrayList,每次的移除操作是remove(0),ArrayList處理remove效率並不高尤其還是remove(0)的操作,因此這裡替換成LinkedList會更好一些。

接著先看第23行~第63行的判斷,它的判斷邏輯是假如當前在使用的Connection數量大於或等於最大可用的Connection數量,那麼獲取當前正在使用的Connection列表中的第一個Connection做一個判斷:

  1. 如果當前Connection執行時間已經超過了指定的Connection最大超時時間,那麼原Connection如果不是自動Commit的,資料回滾,新建一個Connection,原Connection失效
  2. 如果當前Connection執行時間沒有超過指定的Connection最大超時時間,那麼使用wait方法等待

最後回到第17行~第23行的判斷,即當前在使用的Connection數量小於最大可用的Connection數量,那麼此時直接new一個PooledConnection出來,看一下PooledDataSource的getConnection()方法實現:

1 public Connection getConnection() throws SQLException {
2     return doGetConnection(username, password);
3 }

繼續跟程式碼doGetConnection方法:

 1 private Connection doGetConnection(String username, String password) throws SQLException {
 2     Properties props = new Properties();
 3     if (driverProperties != null) {
 4       props.putAll(driverProperties);
 5     }
 6     if (username != null) {
 7       props.setProperty("user", username);
 8     }
 9     if (password != null) {
10       props.setProperty("password", password);
11     }
12     return doGetConnection(props);
13 }

這裡就是先設定一下配置的屬性,繼續跟第12行的方法實現:

1 private Connection doGetConnection(Properties properties) throws SQLException {
2     initializeDriver();
3     Connection connection = DriverManager.getConnection(url, properties);
4     configureConnection(connection);
5     return connection;
6 }

到了這裡就是我們比較熟悉的程式碼了。

第2行的程式碼意思是MyBatis維護了一個Driver池registeredDrivers,如果我們的Driver不在Driver池裡面,那麼會嘗試使用Class.forName方法初始化一下,成功的話加入Driver池中。

第3行的程式碼不說了,使用DriverManager的getConnection方法獲取Connection,第4行的程式碼配置一下Connection,主要就是設定一下自動提交屬性與事物隔離級別。

最後將生成的Connection返回出去,完成生成Connection的流程。

 

為Connection生成代理

上面解析了生成Connection的流程,程式碼到這裡還沒完還有一步,看一下PooledConnection的構造方法:

1 public PooledConnection(Connection connection, PooledDataSource dataSource) {
2     this.hashCode = connection.hashCode();
3     this.realConnection = connection;
4     this.dataSource = dataSource;
5     this.createdTimestamp = System.currentTimeMillis();
6     this.lastUsedTimestamp = System.currentTimeMillis();
7     this.valid = true;
8     this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
9 }

這裡第8行的程式碼會為生成的Connection建立一個代理,PooledConnection本身就實現了InvocationHandler介面,看一下代理內容是什麼,invoke方法的實現:

 1 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 2     String methodName = method.getName();
 3     if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
 4       dataSource.pushConnection(this);
 5       return null;
 6     } else {
 7       try {
 8         if (!Object.class.equals(method.getDeclaringClass())) {
 9           // issue #579 toString() should never fail
10           // throw an SQLException instead of a Runtime
11           checkConnection();
12         }
13         return method.invoke(realConnection, args);
14       } catch (Throwable t) {
15         throw ExceptionUtil.unwrapThrowable(t);
16       }
17     }
18 }

這一步操作主要是為了處理close方法的,看一下pushConnection方法的實現:

 1 protected void pushConnection(PooledConnection conn) throws SQLException {
 2 
 3     synchronized (state) {
 4       state.activeConnections.remove(conn);
 5       if (conn.isValid()) {
 6         if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
 7           state.accumulatedCheckoutTime += conn.getCheckoutTime();
 8           if (!conn.getRealConnection().getAutoCommit()) {
 9             conn.getRealConnection().rollback();
10           }
11           PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
12           state.idleConnections.add(newConn);
13           newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
14           newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
15           conn.invalidate();
16           if (log.isDebugEnabled()) {
17             log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
18           }
19           state.notifyAll();
20         } else {
21           state.accumulatedCheckoutTime += conn.getCheckoutTime();
22           if (!conn.getRealConnection().getAutoCommit()) {
23             conn.getRealConnection().rollback();
24           }
25           conn.getRealConnection().close();
26           if (log.isDebugEnabled()) {
27             log.debug("Closed connection " + conn.getRealHashCode() + ".");
28           }
29           conn.invalidate();
30         }
31       } else {
32         if (log.isDebugEnabled()) {
33           log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
34         }
35         state.badConnectionCount++;
36       }
37     }
38 }

程式碼的邏輯簡單來說就是當呼叫close方法的時候,如果當前空閒Connection列表中的Connection數量小於指定空閒Connection列表中的數量(第二個判斷connectionTypeCode的值為275950209,不知道是幹什麼的),那麼會為原Connection生成一個PooledConnection並加入空閒Connection列表中。

如果不滿足上面的條件,那麼就直接呼叫Connection的close()方法並且讓原Connection失效。

相關文章