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="http://www.Springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://www.Springframework.org/schema/beans 5 http://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