Spring源碼深度解析之資料庫連接JDBC

Spring源碼深度解析之資料庫連接JDBC

  JDBC(Java Data Base Connectivity,Java資料庫連接)是一種用於執行SQL語句的Java API,可以為多種關係資料庫提供統一訪問,它由一組用Java語言編寫的類和介面組成。JDBC為資料庫開發人員提供了一個標準的API,據此可以構建更高級的工具和介面,是資料庫開發人員能夠用純Java API編寫資料庫應用程式,並且可跨平台運行,並且不受資料庫供應商的限制。

  JDBC連接資料庫流程及原理如下:

  (1)在開發環境中載入指定資料庫的驅動程式。接下來的試驗中,使用的資料庫是MySQL,所以需要去下載MySQl支援JDBC的驅動程式,將下載得到的驅動程式載入進開發環境中。

  (2)在Java程式中載入驅動程式。在Java程式中,可以通過Class.forName(「指定資料庫的驅動程式」)的方式來嘉愛添加到開發環境中的驅動程式,例如載入MySQL的數據驅動程式的程式碼為Class.forName(「com.mysql.jdbc.Driver」)。

  (3)創建數據連接對象。通過DriverManager類創建資料庫連接對象Connection。DriverManager類作用於Java程式和JDBC驅動程式之間,用於檢查所載入的驅動程式是否可以建立連接,然後通過它的getConnection方法根據資料庫的URL、用戶名和密碼,創建一個JDBC Connection對象。例如:Connection connection = DriverManager.getConnection(「連接資料庫的URL」,」用戶名」,」密碼」)。其中URL=協議名+IP地址(域名)+埠+資料庫名稱;用戶名和密碼是指登錄資料庫時所使用的用戶名和密碼。具體示例創建MySQL的資料庫連接程式碼如下:

Connection connectMySQL = DriverManager.getConnection(「jdbc:mysql://localhost:3306/myuser」,」root」,」root」);

  (4)創建Statement對象。Statement類的主要是用於執行靜態SQL語句並返回它所生產結果的對象。通過Conncetion對象的createStatement()方法可以創建一個Statement對象。例如:Statement statement = connection.createStatement()。具體示例創建Statement對象程式碼如下:

Statemetn statementMySQL = connectMySQL.createStatement();

  (5)調用Statement對象的相關方法執行相應的SQL語句。通過executeUpdate()方法來對數據更新,包括插入和刪除等操作,例如向staff表中插入一條數據的程式碼:

statement.excuteUpdate(「INSERT INTO staff(name, age, sex, address, depart, worklen, wage)」+ 「VALUES(『Tom1』, 321, 『M』,』China』,』Personnel』,』3』,』3000』)」);

  通過調用Statement對象的executeQuery()方法進行數據的查詢,而查詢的結果會得到ResultSet對象,ResultSet表示執行查詢資料庫後返回的數據的集合,ResultSet對象具有科研指向當前數據行的指針。通過該對象的next()方法,使得指針指向下一行,然後將數據以列號或者欄位名取出。如果當next()方法返回null,則表示下一行中沒有數據存在。使用示例程式碼如下:

ResultSet resultSet = statement.executeQuery(「select * from staff」);

  (6)關閉資料庫連接。使用完資料庫或者不需要訪問資料庫時,通過Connection的close()方法及時關閉資料庫。

 一、Spring連接資料庫程式實現(JDBC)

      Spring中的JDBC連接與直接使用JDBC去連接還是有所差別的,Spring對JDBC做了大量的封裝,消除了冗餘程式碼,使得開發量大大減小。下面通過一個小例子讓大家簡單認識Spring中的JDBC操作。

  (1)創建數據表結構

1 CREATE table 'user'(
2     'id' int(11) NOT NULL auto_increment,
3     'name' varchar(255) default null,
4     'age' int(11) default null,
5     'sex' varchar(255) default null,
6     primary key ('id')
7 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  (2)創建對應數據表的PO。

1 public class User{
2     private int id;
3     private String name;
4     private int age;
5     private String set;
6     //省略get/set方法
7 }

  (3)創建表與實體間的映射

1 public class UserRowMapper implements RowMapper{
2     @Override
3     public Object mapRow(ResultSet set, int index) throws SQLException {
4         User person = new User(set.getInt("id"), set.getString("name"), set.getInt("age"), set.getString("sex"));
5         return person;
6     }
7 }

  (4)創建數據操作介面

1 public interface UserService{
2     public void save(User user);
3     public List<User> getUsers();
4 }

  (5)創建數據操作介面實現類

 1 public class UserServiceImpl implements UserService{
 2     private JdbcTemplate jdbcTemplate;
 3 
 4     //設置數據源
 5     public void setDataSource(DataSource dataSource){
 6         this.jdbcTemplate = new JdbcTemplate(dataSource);
 7     }
 8 
 9     public void save(User user){
10         jdbcTemplate.update("insert into user(name, age, sex) value(?, ?, ?)",
11                 new Object[] {user.getName(), user.getAge(), user.getSex()},
12                 new int[] {java.sql.Types.VARCHAR, java.sql.Types,INTEGER, java.sql.Types.VARCHAR});
13     }
14 
15     @SuppressWarnings("unchecked")
16     public List<User> getUser() {
17         List<User> list = jdbcTemplate.query("select * from user", new UserRowMapper());
18         return list;
19     }
20 }

  (6)創建Spring配置文件

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <beans xmlns="//www.Springframework.org/schema/beans"
 3     xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
 4     xsi:schemaLocation="//www.Springframework.org/schema/beans
 5         //www.Springframework.org/schema/beans/Spring-beans-2.5.xsd">
 6     <!--配置數據源-->
 7     <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
 8         <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
 9         <property name="uri" value="jdbc:mysql://localhost:3306/lexueba"/>
10         <property name="username" value="root"/>
11         <property name="password" value="haojia0421xixi" />
12         <!--連接池啟動時的初始值-->
13         <property name="initialSize" value="1"/>
14         <!--連接池的最大值-->
15         <property name="maxActive" value="300"/>
16         <!--最大空閑值,當經過一個最高峰時間後,連接池可以慢慢將已經用不到的連接慢慢釋放一部分,一直減少到maxIdle為止-->
17         <property name="maxIdle" value="2"/>
18         <!--最小空閑值,當空閑的連接數少於閥值時,連接池就會預申請去一些連接,以免洪峰來時來不及申請-->
19         <property name="minIdle" value="1"/>
20     </bean>
21 
22     <!--配置業務bean:PersonServiceBean-->
23     <bean id="userService" class="service.UserServiceImpl">
24         <!--向屬性DataSource注入數據源-->
25         <property name="dataSource" ref="dataSource"/>
26     </bean>
27 </beans>

  (7)測試

 1 public class SpringJDBCTest{
 2     public static void main(String[] args) {
 3         ApplicationContext act = new ClassPathXmlApplicationContext("bean.xml");
 4         UserService userService = (UserService) act.getBean("userService");
 5         User user = new User();
 6         user.setName("張三");
 7         user.setAge(20);
 8         user.setSex("男");
 9         //保存一條記錄
10         userService.save(user);
11 
12         List<User> person1 = userService.getUser();
13         Systemout.out.println("得到所有的User");
14         for (User person2:person1) {
15             System.out.println(person2.getId() + " " + person2.getName() + " " + person2.getAge() + " " + person2.getSex());
16         }
17     }
18 }

 二、sava/update功能的實現

  我們以上面的例子為基礎開始分析Spring中對JDBC的支援,首先尋找整個功能的切入點,在示例中我們可以看到所有的資料庫操作都封裝在了UserServiceImpl中,而UserServiceImple中的所有資料庫操作又以其內部屬性jdbcTemplate為基礎。這個jdbcTemplate可以作為源碼分析的切入點,我們一起看看它是如何實現定義又是如何被初始化的。

  在UserServiceImple中jdbcTemplate的初始化是從setDataSource函數開始的,DataSource實例通過參數注入,DataSource的創建過程是引入第三方的連接池,這裡不做過多的介紹。DataSource是整個資料庫操作的基礎,裡面封裝了整個資料庫的連接資訊。我們首先以保存實體類為例進行程式碼跟蹤。

1 public void save(User user){
2         jdbcTemplate.update("insert into user(name, age, sex) value(?, ?, ?)",
3         new Object[] {user.getName(), user.getAge(), user.getSex()},
4         new int[] {java.sql.Types.VARCHAR, java.sql.Types,INTEGER, java.sql.Types.VARCHAR});
5         }

  對於保存一個實體類來講,在操作中我們只需要提供SQL語句及語句中對應的參數和參數類型,其他操作便可以交由Spring來完成了,這些工作到底包括什麼呢?進入jdbcTemplate中的update方法:

1     @Override
2     public int update(String sql, @Nullable PreparedStatementSetter pss) throws DataAccessException {
3         return update(new SimplePreparedStatementCreator(sql), pss);
4     }
5 
6     @Override
7     public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException {
8         return update(sql, newArgTypePreparedStatementSetter(args, argTypes));
9     }

  進入update方法後,Spring並不是急於進入核心處理操作,而是做足了準備工作,使用ArgPreparedStatementSetter對參數與參數類型進行封裝,同時又使用Simple PreparedStatementCreator對SQL語句進行封裝。

  經過數據封裝後便可以進入了核心的數據處理程式碼了。

 1     protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss)
 2             throws DataAccessException {
 3 
 4         logger.debug("Executing prepared SQL update");
 5 
 6         return updateCount(execute(psc, ps -> {
 7             try {
 8                 if (pss != null) {
 9                     //設置PreparedStatement所需的全部參數
10                     pss.setValues(ps);
11                 }
12                 int rows = ps.executeUpdate();
13                 if (logger.isTraceEnabled()) {
14                     logger.trace("SQL update affected " + rows + " rows");
15                 }
16                 return rows;
17             }
18             finally {
19                 if (pss instanceof ParameterDisposer) {
20                     ((ParameterDisposer) pss).cleanupParameters();
21                 }
22             }
23         }));
24     }

  如果讀者了解過其他操作方法,可以知道execute方法是最基礎的操作。而其他操作比如update、query等方法則是傳入不同的PreparedStatementCallback參數來執行不同的邏輯。

(一)基礎方法execute

  execute作為資料庫操作的核心入口,將大多數資料庫操作相同的步驟統一封裝,而將個性化的操作使用參數PreparedStatementCallback進行回調。

 1     public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
 2             throws DataAccessException {
 3 
 4         Assert.notNull(psc, "PreparedStatementCreator must not be null");
 5         Assert.notNull(action, "Callback object must not be null");
 6         if (logger.isDebugEnabled()) {
 7             String sql = getSql(psc);
 8             logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
 9         }
10 
11         //獲取資料庫連接
12         Connection con = DataSourceUtils.getConnection(obtainDataSource());
13         PreparedStatement ps = null;
14         try {
15             ps = psc.createPreparedStatement(con);
16             //應用用戶設定的輸入參數
17             applyStatementSettings(ps);
18             //調用回調函數
19             T result = action.doInPreparedStatement(ps);
20             handleWarnings(ps);
21             return result;
22         }
23         catch (SQLException ex) {
24             // Release Connection early, to avoid potential connection pool deadlock
25             // in the case when the exception translator hasn't been initialized yet.
26             //釋放資料庫連接避免當異常轉換器沒有被初始化的時候出現潛在的連接池死鎖
27             if (psc instanceof ParameterDisposer) {
28                 ((ParameterDisposer) psc).cleanupParameters();
29             }
30             String sql = getSql(psc);
31             psc = null;
32             JdbcUtils.closeStatement(ps);
33             ps = null;
34             DataSourceUtils.releaseConnection(con, getDataSource());
35             con = null;
36             throw translateException("PreparedStatementCallback", sql, ex);
37         }
38         finally {
39             if (psc instanceof ParameterDisposer) {
40                 ((ParameterDisposer) psc).cleanupParameters();
41             }
42             JdbcUtils.closeStatement(ps);
43             DataSourceUtils.releaseConnection(con, getDataSource());
44         }
45     }

  以上方法對常用操作進行了封裝,包括如下幾項內容。

  1、獲取資料庫連接

  獲取資料庫連接池也並非直接使用dataSource.getConnection()方法那麼簡單,同樣也考慮了諸多情況。

 1     public static Connection doGetConnection(DataSource dataSource) throws SQLException {
 2         Assert.notNull(dataSource, "No DataSource specified");
 3 
 4         ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
 5         if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
 6             conHolder.requested();
 7             if (!conHolder.hasConnection()) {
 8                 logger.debug("Fetching resumed JDBC Connection from DataSource");
 9                 conHolder.setConnection(fetchConnection(dataSource));
10             }
11             return conHolder.getConnection();
12         }
13         // Else we either got no holder or an empty thread-bound holder here.
14 
15         logger.debug("Fetching JDBC Connection from DataSource");
16         Connection con = fetchConnection(dataSource);
17 
18         //當前執行緒支援同步
19         if (TransactionSynchronizationManager.isSynchronizationActive()) {
20             try {
21                 // Use same Connection for further JDBC actions within the transaction.
22                 // Thread-bound object will get removed by synchronization at transaction completion.
23                 //在事務中使用同一資料庫連接
24                 ConnectionHolder holderToUse = conHolder;
25                 if (holderToUse == null) {
26                     holderToUse = new ConnectionHolder(con);
27                 }
28                 else {
29                     holderToUse.setConnection(con);
30                 }
31                 //記錄資料庫連接
32                 holderToUse.requested();
33                 TransactionSynchronizationManager.registerSynchronization(
34                         new ConnectionSynchronization(holderToUse, dataSource));
35                 holderToUse.setSynchronizedWithTransaction(true);
36                 if (holderToUse != conHolder) {
37                     TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
38                 }
39             }
40             catch (RuntimeException ex) {
41                 // Unexpected exception from external delegation call -> close Connection and rethrow.
42                 releaseConnection(con, dataSource);
43                 throw ex;
44             }
45         }
46 
47         return con;
48     }

  在資料庫連接方面,Spring主要考慮的是關於事務方面的處理,基於事務處理的特殊性,Spring需要保證執行緒中的資料庫操作都是使用同一事務連接。

  2、應用用戶設定的輸入參數。

 1     protected void applyStatementSettings(Statement stmt) throws SQLException {
 2         int fetchSize = getFetchSize();
 3         if (fetchSize != -1) {
 4             stmt.setFetchSize(fetchSize);
 5         }
 6         int maxRows = getMaxRows();
 7         if (maxRows != -1) {
 8             stmt.setMaxRows(maxRows);
 9         }
10         DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout());
11     }

  setFetchSize最主要是為了減少網路交互次數設計的。訪問ResultSet時,如果它每次只從伺服器上讀取一行數據,則會產生大量的開銷。setFetchSize的意思是當調用rs.next時,ResultSet會一次性從伺服器上取得多少行數據回來,這樣在下次rs.next時,它可以直接從記憶體中獲取數據而不需要網路交互,提高效率。這個設置可能會被某些JDBC驅動忽略,而且設置過大會造成記憶體的上升。

  setMaxRows將此Statement對象生成的所有ResultSet對象可以包含的最大行數限制設置為給定數。

  3、調用回調函數

  處理一些通用方法外的個性化處理,也就是PreparedStatementCallback類型的參數的doInPreparedStatement方法的回調。

  4、警告處理

 1     protected void handleWarnings(Statement stmt) throws SQLException {
 2         //當設置為忽略警告時只嘗試列印日誌
 3         if (isIgnoreWarnings()) {
 4             if (logger.isDebugEnabled()) {
 5                 //如果日誌開啟的情況下列印日誌
 6                 SQLWarning warningToLog = stmt.getWarnings();
 7                 while (warningToLog != null) {
 8                     logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + "', error code '" +
 9                             warningToLog.getErrorCode() + "', message [" + warningToLog.getMessage() + "]");
10                     warningToLog = warningToLog.getNextWarning();
11                 }
12             }
13         }
14         else {
15             handleWarnings(stmt.getWarnings());
16         }
17     }

  這裡用到了一個類SQLWarning,SQLWarning提供關於資料庫訪問警告資訊的異常。這些警告直接鏈接到導致報告警告的方法所在的對象。警告可以從Connection、Statement和ResultSet對象中獲得。試圖在已經關閉的連接上獲取警告將導致拋出異常。類似地,試圖在已經關閉的語句上或已經關閉的結果集上獲取警告也將導致拋出異常。注意,關閉語句時還會關閉它可能生成的結果集。

  很多人不是很理解什麼情況下會產生警告而不是異常,在這裡給讀者提示個最常見的警告:DataTruncation,DataTruncation直接繼承SQLWaring,由於某種原因意外地截斷數據值時會以DataTruncation警告形式報告異常。

  對於警告的處理方式並不是直接拋出異常,出現警告很可能會出現數據錯誤,但是,並不一定會影響程式執行,所以用戶可以自己設置處理警告的方式,如默認的是忽略警告,當出現警告時只列印警告日誌,而另一種方式只直接拋出異常。

  5、資源釋放

  資料庫的連接釋放並不是直接調用了Connection的API的close方法。考慮到存在事務的情況,如果當前執行緒存在事務,那麼說明在當前執行緒中存在共用的資料庫連接,在這種情況下直接使用ConnectionHolder中的released方法進行連接數減1,而不是真正的釋放連接。進入DataSourceUtils類的releaseConnection函數:

 1     public static void releaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) {
 2         try {
 3             doReleaseConnection(con, dataSource);
 4         }
 5         catch (SQLException ex) {
 6             logger.debug("Could not close JDBC Connection", ex);
 7         }
 8         catch (Throwable ex) {
 9             logger.debug("Unexpected exception on closing JDBC Connection", ex);
10         }
11     }

   上面函數又調用了本類中的doReleaseConnection函數:

 1     public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException {
 2         if (con == null) {
 3             return;
 4         }
 5         if (dataSource != null) {
 6             //當前執行緒存在事務的情況下說明存在共用資料庫連接直接使用ConnectionHolder中的released方法進行連接數減1,而不是真正的釋放連接。
 7             ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
 8             if (conHolder != null && connectionEquals(conHolder, con)) {
 9                 // It's the transactional Connection: Don't close it.
10                 conHolder.released();
11                 return;
12             }
13         }
14         doCloseConnection(con, dataSource);
15     }

 (二)Update函數

 1     protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss)
 2             throws DataAccessException {
 3 
 4         logger.debug("Executing prepared SQL update");
 5 
 6         return updateCount(execute(psc, ps -> {
 7             try {
 8                 if (pss != null) {
 9                     //設置PreparedStatement所需的全部參數
10                     pss.setValues(ps);
11                 }
12                 int rows = ps.executeUpdate();
13                 if (logger.isTraceEnabled()) {
14                     logger.trace("SQL update affected " + rows + " rows");
15                 }
16                 return rows;
17             }
18             finally {
19                 if (pss instanceof ParameterDisposer) {
20                     ((ParameterDisposer) pss).cleanupParameters();
21                 }
22             }
23         }));
24     }

  其中用於真正執行SQL的int rows = ps.executeUpdate();沒有太多需要講解的,因為我們平時在直接使用JDBC方式進行調用的時候經常使用此方法。但是,對於設置輸入參數的函數pss.setValues(ps);,我們有必要去深入研究一下。在沒有分析程式碼之前,我們至少可以知道其功能,不妨再回顧下Spring中使用SQL的執行過程,直接使用:

1 jdbcTemplate.update("insert into user(name, age, sex) value(?, ?, ?)",
2         new Object[] {user.getName(), user.getAge(), user.getSex()},
3         new int[] {java.sql.Types.VARCHAR, java.sql.Types,INTEGER, java.sql.Types.VARCHAR});

  SQL語句對應的參數的類型清晰明了,這都歸功於Spring為我們做了封裝,而真正的JDBC調用其實非常繁瑣,你需要這麼做:

1 PreparedStatement updateSales = con.prepareStatement("insert into user(name, age, sex) values(?, ?, ?)");
2         updateSales.setString(1, user.getName());
3         updateSales.setInt(2, user.getAge());
4         updateSales.setString(3, user.getSex());

  那麼看看Spring是如何做到封裝上面的操作呢?
  首先,所有的操作都是以pss.setValues(ps)為入口的。還記得我們之前的分析路程嗎?這個pss所代表的當前類正是ArgumentTypePreparedStatementSetter。其中的setValues方法如下:

 1     public void setValues(PreparedStatement ps) throws SQLException {
 2         int parameterPosition = 1;
 3         if (this.args != null && this.argTypes != null) {
 4             //遍歷每個參數以作類型匹配和轉換
 5             for (int i = 0; i < this.args.length; i++) {
 6                 Object arg = this.args[i];
 7                 //如果是集合類型則需要進入集合類內部遞歸解析集合內部屬性
 8                 if (arg instanceof Collection && this.argTypes[i] != Types.ARRAY) {
 9                     Collection<?> entries = (Collection<?>) arg;
10                     for (Object entry : entries) {
11                         if (entry instanceof Object[]) {
12                             Object[] valueArray = ((Object[]) entry);
13                             for (Object argValue : valueArray) {
14                                 doSetValue(ps, parameterPosition, this.argTypes[i], argValue);
15                                 parameterPosition++;
16                             }
17                         }
18                         else {
19                             //解析當前屬性
20                             doSetValue(ps, parameterPosition, this.argTypes[i], entry);
21                             parameterPosition++;
22                         }
23                     }
24                 }
25                 else {
26                     doSetValue(ps, parameterPosition, this.argTypes[i], arg);
27                     parameterPosition++;
28                 }
29             }
30         }
31     }

  對單個參數及類型的匹配處理:

1     protected void doSetValue(PreparedStatement ps, int parameterPosition, int argType, Object argValue)
2             throws SQLException {
3 
4         StatementCreatorUtils.setParameterValue(ps, parameterPosition, argType, argValue);
5     }

   上述函數調用了StatementCreatorUtils類的setParameterValue方法,進入:

1     public static void setParameterValue(PreparedStatement ps, int paramIndex, SqlParameter param,
2             @Nullable Object inValue) throws SQLException {
3 
4         setParameterValueInternal(ps, paramIndex, param.getSqlType(), param.getTypeName(), param.getScale(), inValue);
5     }

   調用了本類的setParameterValueInternal函數,繼續進入:

 1     private static void setParameterValueInternal(PreparedStatement ps, int paramIndex, int sqlType,
 2             @Nullable String typeName, @Nullable Integer scale, @Nullable Object inValue) throws SQLException {
 3 
 4         String typeNameToUse = typeName;
 5         int sqlTypeToUse = sqlType;
 6         Object inValueToUse = inValue;
 7 
 8         // override type info?
 9         if (inValue instanceof SqlParameterValue) {
10             SqlParameterValue parameterValue = (SqlParameterValue) inValue;
11             if (logger.isDebugEnabled()) {
12                 logger.debug("Overriding type info with runtime info from SqlParameterValue: column index " + paramIndex +
13                         ", SQL type " + parameterValue.getSqlType() + ", type name " + parameterValue.getTypeName());
14             }
15             if (parameterValue.getSqlType() != SqlTypeValue.TYPE_UNKNOWN) {
16                 sqlTypeToUse = parameterValue.getSqlType();
17             }
18             if (parameterValue.getTypeName() != null) {
19                 typeNameToUse = parameterValue.getTypeName();
20             }
21             inValueToUse = parameterValue.getValue();
22         }
23 
24         if (logger.isTraceEnabled()) {
25             logger.trace("Setting SQL statement parameter value: column index " + paramIndex +
26                     ", parameter value [" + inValueToUse +
27                     "], value class [" + (inValueToUse != null ? inValueToUse.getClass().getName() : "null") +
28                     "], SQL type " + (sqlTypeToUse == SqlTypeValue.TYPE_UNKNOWN ? "unknown" : Integer.toString(sqlTypeToUse)));
29         }
30 
31         if (inValueToUse == null) {
32             setNull(ps, paramIndex, sqlTypeToUse, typeNameToUse);
33         }
34         else {
35             setValue(ps, paramIndex, sqlTypeToUse, typeNameToUse, scale, inValueToUse);
36         }
37     }

 三、query功能的實現

  在之前的章節中我們介紹了update方法的功能實現。那麼在資料庫操作中查找操作也是使用非常高的函數,同樣我們也需要了解它的實現過程。使用方法如下:

1 List<User> list = jdbcTemplate.query("select * from user where age=?",
2         new Object[][20], new int[]{java.sql.Types.INTEGER}, new UserRowMapper());

  跟蹤jdbcTemplate的query方法:

1     @Override
2     public <T> List<T> query(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {
3         return result(query(sql, args, argTypes, new RowMapperResultSetExtractor<>(rowMapper)));
4     }
1     @Override
2     @Nullable
3     public <T> T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor<T> rse) throws DataAccessException {
4         return query(sql, newArgTypePreparedStatementSetter(args, argTypes), rse);
5     }

   上面的函數中和update方法中同樣使用了 newArgTypePreparedStatementSetter。

1     public <T> T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
2         return query(new SimplePreparedStatementCreator(sql), pss, rse);
3     }
 1     public <T> T query(
 2             PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)
 3             throws DataAccessException {
 4 
 5         Assert.notNull(rse, "ResultSetExtractor must not be null");
 6         logger.debug("Executing prepared SQL query");
 7 
 8         return execute(psc, new PreparedStatementCallback<T>() {
 9             @Override
10             @Nullable
11             public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
12                 ResultSet rs = null;
13                 try {
14                     if (pss != null) {
15                         //設置PreparedStatement所需的全部參數
16                         pss.setValues(ps);
17                     }
18                     rs = ps.executeQuery();
19                     return rse.extractData(rs);
20                 }
21                 finally {
22                     JdbcUtils.closeResultSet(rs);
23                     if (pss instanceof ParameterDisposer) {
24                         ((ParameterDisposer) pss).cleanupParameters();
25                     }
26                 }
27             }
28         });
29     }

  可以看到整體套路和update差不多,只不過在回調類PreparedStatementCallback的實現中使用的是ps.executeQuery()執行查詢操作,而且在返回方法上也做了一些額外的處理。

  rse.extractData(rs)方法負責將結果進行封裝並轉換到POJO,rse當前代表的類為RowMapperResultSetExtractor,而在構造RowMapperResultSetExtractor的時候我們又將自定義的rowMapper設置了進去。調用程式碼如下:

1     public List<T> extractData(ResultSet rs) throws SQLException {
2         List<T> results = (this.rowsExpected > 0 ? new ArrayList<>(this.rowsExpected) : new ArrayList<>());
3         int rowNum = 0;
4         while (rs.next()) {
5             results.add(this.rowMapper.mapRow(rs, rowNum++));
6         }
7         return results;
8     }

  上面的程式碼並沒有什麼負責的邏輯,只是對返回的結果遍歷並以此使用rowMapper進行轉換。

  之前降了update方法以及query方法,使用這兩個函數示例的SQL都是帶參數值的,也就是帶有「?」的,那麼還有另一種情況是不帶有「?」的,Spring中使用的是另一種處理方式,例如:

List<User> list = jdbcTemplate.query("select * from user", new UserRowMapper());

  跟蹤進入:

 1     public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
 2         Assert.notNull(sql, "SQL must not be null");
 3         Assert.notNull(rse, "ResultSetExtractor must not be null");
 4         if (logger.isDebugEnabled()) {
 5             logger.debug("Executing SQL query [" + sql + "]");
 6         }
 7 
 8         /**
 9          * Callback to execute the query.
10          */
11         class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
12             @Override
13             @Nullable
14             public T doInStatement(Statement stmt) throws SQLException {
15                 ResultSet rs = null;
16                 try {
17                     rs = stmt.executeQuery(sql);
18                     return rse.extractData(rs);
19                 }
20                 finally {
21                     JdbcUtils.closeResultSet(rs);
22                 }
23             }
24             @Override
25             public String getSql() {
26                 return sql;
27             }
28         }
29 
30         return execute(new QueryStatementCallback());
31     }

  與之前的query方法最大的不同是少了參數及參數類型的傳遞,自然也少了PreparedStatementSetter類型的封裝。既然少了PreparedStatementSetter類型的傳入,調用的execute方法自然也會有所改變了。

 1     public <T> T execute(StatementCallback<T> action) throws DataAccessException {
 2         Assert.notNull(action, "Callback object must not be null");
 3 
 4         Connection con = DataSourceUtils.getConnection(obtainDataSource());
 5         Statement stmt = null;
 6         try {
 7             stmt = con.createStatement();
 8             applyStatementSettings(stmt);
 9             T result = action.doInStatement(stmt);
10             handleWarnings(stmt);
11             return result;
12         }
13         catch (SQLException ex) {
14             // Release Connection early, to avoid potential connection pool deadlock
15             // in the case when the exception translator hasn't been initialized yet.
16             String sql = getSql(action);
17             JdbcUtils.closeStatement(stmt);
18             stmt = null;
19             DataSourceUtils.releaseConnection(con, getDataSource());
20             con = null;
21             throw translateException("StatementCallback", sql, ex);
22         }
23         finally {
24             JdbcUtils.closeStatement(stmt);
25             DataSourceUtils.releaseConnection(con, getDataSource());
26         }
27     }

  這個execute與之前的execute並無太大的差別,都是做一些常規的處理,諸如獲取連接、釋放連接等,但是,有一個地方是不一樣的,就是statement的創建。這裡直接使用connection創建,而帶有參數的SQL使用的是PreparedStatementCreator類來創建的。一個是普通的Statement,另一個是PreparedStatement,兩者究竟是何區別呢?

  PreparedStatement介面繼承Statement,並與之在兩方面有所不同。

  1、PreparedStatement實例包含已經編譯的SQL語句。這就是使語句「準備好」。包含於PreparedStatement對象中的SQL語句可具有一個或者多個IN參數。IN參數的值在SQL語句創建時未被指定。相反的,該語句為每個IN參數保留一個問號(「?」)作為佔位符。每個問號的值必須在該語句執行之前,通過適當的setXXX方法來提供。

  2、由於PreparedStatement對象已預編譯過,所以其執行速度要快於Statement對象。因此,多次執行的SQL語句經常創建為PreparedStatement對象,以提高效率。

  作為Statement的子類,PreparedStatement繼承了Statement的所有功能。另外,它還添加了一整套方法,用於設置發送給資料庫以取代IN參數佔位符的值。同時,三種方法execute、executeQuery和executeUpdate已被更改以使之不再需要參數。這些方法的Statement形式(接收SQL語句參數的形式)不應該用於PreparedStatement對象。

 四、queryForObject

  Spring中不僅為我們提供了query方法,還在此基礎上做了封裝,提供了不同類型的query方法。

  我們以queryForObject為例,來討論一下Spring是如何在返回結果的基礎上進行封裝的。

1     public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
2         return queryForObject(sql, getSingleColumnRowMapper(requiredType));
3     }
1     public <T> T queryForObject(String sql, RowMapper<T> rowMapper) throws DataAccessException {
2         List<T> results = query(sql, rowMapper);
3         return DataAccessUtils.nullableSingleResult(results);
4     }

  其實最大的不同還是對於RowMapper的使用,SingleColumnRowMapper類中的mapRow:

 1     public T mapRow(ResultSet rs, int rowNum) throws SQLException {
 2         // Validate column count.
 3         //驗證返回結果
 4         ResultSetMetaData rsmd = rs.getMetaData();
 5         int nrOfColumns = rsmd.getColumnCount();
 6         if (nrOfColumns != 1) {
 7             throw new IncorrectResultSetColumnCountException(1, nrOfColumns);
 8         }
 9 
10         // Extract column value from JDBC ResultSet.
11         //抽取第一個結果進行處理
12         Object result = getColumnValue(rs, 1, this.requiredType);
13         if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) {
14             // Extracted value does not match already: try to convert it.
15             //轉換到對象的類型
16             try {
17                 return (T) convertValueToRequiredType(result, this.requiredType);
18             }
19             catch (IllegalArgumentException ex) {
20                 throw new TypeMismatchDataAccessException(
21                         "Type mismatch affecting row number " + rowNum + " and column type '" +
22                         rsmd.getColumnTypeName(1) + "': " + ex.getMessage());
23             }
24         }
25         return (T) result;
26     }

  對應的類型轉換函數:

 1     protected Object convertValueToRequiredType(Object value, Class<?> requiredType) {
 2         if (String.class == requiredType) {
 3             return value.toString();
 4         }
 5         else if (Number.class.isAssignableFrom(requiredType)) {
 6             if (value instanceof Number) {
 7                 // Convert original Number to target Number class.
 8                 //轉換原始的Number類型的實體到Number類
 9                 return NumberUtils.convertNumberToTargetClass(((Number) value), (Class<Number>) requiredType);
10             }
11             else {
12                 // Convert stringified value to target Number class.
13                 //轉換String類型的值到Number類
14                 return NumberUtils.parseNumber(value.toString(),(Class<Number>) requiredType);
15             }
16         }
17         else if (this.conversionService != null && this.conversionService.canConvert(value.getClass(), requiredType)) {
18             return this.conversionService.convert(value, requiredType);
19         }
20         else {
21             throw new IllegalArgumentException(
22                     "Value [" + value + "] is of type [" + value.getClass().getName() +
23                     "] and cannot be converted to required type [" + requiredType.getName() + "]");
24         }
25     }

   本文摘自《Spring源碼深度解析》資料庫連接JDBC,作者:郝佳。本文程式碼基於的Spring版本為5.2.4.BUILD-SNAPSHOT,和原書程式碼部分會略有不同。

 

拓展閱讀:
  Spring框架之beans源碼完全解析
  Spring框架之AOP源碼完全解析
  Spring框架之jms源碼完全解析
  Spring框架之spring-web http源碼完全解析
  Spring框架之spring-web web源碼完全解析
       Spring框架之spring-webmvc源碼完全解析
  Spring源碼深度解析之Spring MVC