用過JDBC(Java DataBase Connectivity,Java數據庫連接)的人都知道,JDBC非常臃腫,一點也不可愛。以致於我們每次使用JDBC操作數據庫時,總會忍不住吐槽。為了讓大家少些吐槽,多些舒心;致力於簡化Java開發的Spring果斷出手,簡化了JDBC,把它封裝成為Spring旗下的一個重要模塊。這個模塊就是著名的Spring JDBC。至於簡化了多少,且讓我們先用傳統的JDBC實現一個小項目,再用Spring JDBC對其進行改進,進而比較直觀地了解Spring JDBC對JDBC做的簡化,學習Spring JDBC的基礎知識。
這個例子非常簡單,就往數據庫里插入人的信息,之后查詢出來進行顯示。因此,我們需要創建一個數據庫,一張數據庫表,用於保存人的信息。而這,可以通過打開先前安裝過的MYSQL Workbench,執行以下SQL腳本進行創建:
1 CREATE DATABASE sj_person_jdbc DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; 2 3 USE sj_person_jdbc; 4 5 CREATE TABLE person ( 6 person_id INT NOT NULL AUTO_INCREMENT, # 數據庫表ID 7 person_name VARCHAR(50) NOT NULL, # 名字 8 person_gender VARCHAR(50) NOT NULL, # 性別 9 PRIMARY KEY (person_id) # 添加主鍵 10 ) ENGINE = INNODB;
建好數據庫之后,還需新建一個Java項目,用於實現前文提到的功能。這個項目名叫person,同先前一樣打開IntelliJ IDEA進行創建即可。不同的是,這個項目需與數據庫打交道。因此,還需添加數據庫驅動程序JAR包。數據庫驅動程序JAR包無需額外下載,我們安裝MYSQL的時候已經裝上了。只需打開MYSQL的安裝目錄(默認裝在C:\Program Files (x86)\MySQL\Connector J 8.0\),把文件mysql-connector-java-8.0.23.jar復制到項目的libs目錄即可。
注意:復制數據庫驅動程序JAR包到libs目錄之后,可能需要重啟一下IntelliJ IDEA。不然,IntelliJ IDEA可能不會加載新增的JAR包,導致編譯錯誤。
我們知道使用JDBC操作數據庫之前,首先應該獲取數據庫連接。獲取數據庫連接的方式通常有兩種:一種是通過DriverManager,一種是通過數據源。
通過DriverManager獲取數據庫連接並不是一個好的方式。為什么呢?因為DriverManager並不支持連接池(Connection Pool)。這意味着每次通過DriverManager獲取數據庫連接時都得建立新的連接。這個過程涉及網絡連接的建立,協議的交換,身份的驗證,等等。既耗資源,又費時間,一點也不划算。因此,進行軟件開發的時候,通常不會選用這種方式。
於是,第二種方式出現了。這種方式涉及JDBC提供的一個接口:javax.sql.DataSource。這個接口就是通常所說的數據源。目前,有很多廠商對該接口進行了實現,使之作為一個組件具有把數據庫連接緩存到連接池,以及對連接池里的連接進行管理的功能。這樣,每次通過數據源獲取數據庫連接時,只需從連接池里獲取,無需花費大量的資源和時間建立新的連接;用完之后把連接交還連接池就行。由此可見,通過數據源獲取數據庫連接可以大幅提高性能。正因如此,進行軟件開發的時候,通常選用這種方式。我們的小項目也不例外。
需要了解的是,雖然在這世上實現和提供數據源的廠商遠遠不止一家。但是,常用的數據源總是那樣幾種。比如DBCP,C3P0,Druid,等等。我們的小項目選用的是DBCP。而這,需要我們下載DBCP相關的JAR包並添加到我們的小項目中。如下:
1.Apache Commons DBCP,下載文件commons-dbcp2-2.8.0-bin.zip:
http://commons.apache.org/proper/commons-dbcp/download_dbcp.cgi
2.Apache Commons Pool,下載文件commons-pool2-2.9.0-bin.zip:
http://commons.apache.org/proper/commons-pool/download_pool.cgi
3.Apache Commons Logging,下載文件commons-logging-1.2-bin.zip:
http://commons.apache.org/proper/commons-logging/download_logging.cgi
下載完成之后解壓.zip文件,把下面這些JAR包復制到libs目錄即可:
1.commons-dbcp2-2.8.0.jar
2.commons-pool2-2.9.0.jar
3.commons-logging-1.2.jar
現在,萬事俱備,可以開始寫代碼了。我們的目標是使用JDBC實現兩個功能,即往數據庫里插入人的信息,以及從數據庫里查出人的信息進行顯示。因此,我們需要一個數據模型類,用於保存人的信息。如下所示:
1 package com.dream; 2 3 public class Person { 4 private int id = 0; 5 private String name = null; 6 private String gender = null; 7 8 public int getId() { 9 return this.id; 10 } 11 12 public void setId(int id) { 13 this.id = id; 14 } 15 16 public String getName() { 17 return this.name; 18 } 19 20 public void setName(String name) { 21 this.name = name; 22 } 23 24 public String getGender() { 25 return this.gender; 26 } 27 28 public void setGender(String gender) { 29 this.gender = gender; 30 } 31 }
數據模型Person類定義了三個屬性:id(數據庫表ID),name(名字),gender(性別)。這三個屬性是和數據庫表person的三個字段一一對應的,剛好能夠作為數據模型,在與數據庫打交道時保存人的信息。當然,除了數據模型,我們還需考慮如何定義一個數據存取類,用於插入和查詢人的信息。如下所示:
1 package com.dream; 2 3 import java.sql.*; 4 import java.util.*; 5 import javax.sql.*; 6 import org.apache.commons.dbcp2.*; 7 8 public class DaoPerson { 9 public int addPerson(Person person) { 10 int updatedCount = 0; 11 Connection connection = null; 12 PreparedStatement statement = null; 13 SQLException sqlException = null; 14 try { 15 var sql = " INSERT INTO person" 16 + " (person_name, person_gender)" 17 + " VALUES" 18 + " (?, ?)"; 19 var dataSource = this.getDataSource(); 20 connection = dataSource.getConnection(); 21 statement = connection.prepareStatement(sql); 22 statement.setString(1, person.getName()); 23 statement.setString(2, person.getGender()); 24 updatedCount = statement.executeUpdate(); 25 } catch (SQLException e) { 26 sqlException = e; 27 } finally { 28 if (statement != null) { 29 try { 30 statement.close(); 31 } catch (SQLException e) { 32 if (sqlException == null) { 33 sqlException = e; 34 } 35 } 36 } 37 if (connection != null) { 38 try { 39 connection.close(); 40 } catch (SQLException e) { 41 if (sqlException == null) { 42 sqlException = e; 43 } 44 } 45 } 46 if (sqlException != null) { 47 throw new RuntimeException(sqlException); 48 } 49 } 50 return updatedCount; 51 } 52 53 public List<Person> queryPersonByName(String personName) { 54 List<Person> personList = new ArrayList<>(); 55 Connection connection = null; 56 PreparedStatement statement = null; 57 SQLException sqlException = null; 58 try { 59 var sql = " SELECT" 60 + " person_id, person_name, person_gender" 61 + " FROM" 62 + " person" 63 + " WHERE" 64 + " person_name = ?"; 65 var dataSource = this.getDataSource(); 66 connection = dataSource.getConnection(); 67 statement = connection.prepareStatement(sql); 68 statement.setString(1, personName); 69 var result = statement.executeQuery(); 70 while (result.next()) { 71 var person = new Person(); 72 person.setId(result.getInt(1)); 73 person.setName(result.getString(2)); 74 person.setGender(result.getString(3)); 75 personList.add(person); 76 } 77 } catch (SQLException e) { 78 sqlException = e; 79 } finally { 80 if (statement != null) { 81 try { 82 statement.close(); 83 } catch (SQLException e) { 84 if (sqlException == null) { 85 sqlException = e; 86 } 87 } 88 } 89 if (connection != null) { 90 try { 91 connection.close(); 92 } catch (SQLException e) { 93 if (sqlException == null) { 94 sqlException = e; 95 } 96 } 97 } 98 if (sqlException != null) { 99 throw new RuntimeException(sqlException); 100 } 101 } 102 return personList; 103 } 104 105 private DataSource getDataSource() { 106 var dataSource = new BasicDataSource(); 107 dataSource.setUsername("root"); 108 dataSource.setPassword("123456"); 109 dataSource.setUrl("jdbc:mysql://localhost:3306/sj_person_jdbc"); 110 dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); 111 return dataSource; 112 } 113 }
經過一段時間的努力,我們終於把數據存取類DaoPerson(DAO,Data Access Object,數據存取對象)寫完了。不禁感嘆,該類的代碼真的太長了!卻主要定義了三個方法:getDataSource,addPerson,queryPersonByName。
getDataSource方法只做一件事:創建數據源對象,也就是創建BasicDataSource類的實例。BasicDataSource類是DBCP提供的一種數據源實現,實現了javax.sql.DataSource接口,使之具有緩存數據庫連接到連接池以及對連接池里的連接進行管理的功能。之后,再把數據庫用戶名,密碼,URL以及驅動類這些信息賦給剛剛創建的數據源對象;最后返回數據源對象。這樣,其它代碼就能調用這個方法創建數據源對象,再經數據源對象獲得數據庫連接了。
addPerson方法實現了數據的插入,也就是把人的信息插入數據庫。這個過程涉及這些步驟:
01.調用getDataSource方法創建數據源對象。
02.通過數據源對象獲得數據庫連接對象。
03.通過數據庫連接對象獲得PreparedStatement對象。
04.調用PreparedStatement對象的方法設置名字,性別兩個SQL參數。
05.執行SQL語句插入人的信息。
06.處理SQL語句執行異常。
07.關閉PreparedStatement
08.處理PreparedStatement關閉異常。
09.關閉數據庫連接。
10.處理數據庫連接關閉異常。
queryPersonByName方法實現了數據的查詢,也就是根據人的名字查出人的信息。這個過程涉及這些步驟:
01.調用getDataSource方法創建數據源對象。
02.通過數據源對象獲得數據庫連接對象。
03.通過數據庫連接對象獲得PreparedStatement對象。
04.調用PreparedStatement對象的方法設置名字這個SQL參數。
05.執行SQL語句查出人的信息。
06.處理SQL語句執行異常。
07.關閉PreparedStatement
08.處理PreparedStatement關閉異常。
09.關閉數據庫連接。
10.處理數據庫連接關閉異常。
看完這些步驟之后,想必“我和我的小伙伴們都驚呆了”。addPerson和queryPersonByName方法相比,除了第四步,第五步之外,其它步驟竟然都是一樣的!這意味着我們正在不知疲倦地重復敲寫同樣的代碼(俗稱樣板代碼)實現不同的數據庫操作。
這,不是糟透了嗎?
於是我們不禁追問:“難道就沒有什么辦法能夠消除這些糟糕的樣板代碼,讓事情變得簡單一些,優雅一些嗎?”
當然有的。比如,我們可以寫些數據庫存取類,把JDBC那些樣板代碼封裝起來,爾后使用這些封裝好的類進行數據庫存取,這樣不就可以消除樣板代碼,讓事情簡單起來了嗎?關於這點,從來都是英雄所見略同。Spring也是這樣想的。於是Spring大刀闊斧,把JDBC那些樣板代碼封裝起來,消除了樣板代碼,產出Spring JDBC這個模塊。至於Spring JDBC能夠簡化多少代碼,且讓我們使用Spring JDBC重新實現一下addPerson方法,看看實際的效果。如下所示:
1 public int addPerson(Person person) { 2 var sql = " INSERT INTO person" 3 + " (person_name, person_gender)" 4 + " VALUES" 5 + " (?, ?)"; 6 var dataSource = this.getDataSource(); 7 var jdbcTemplate = new JdbcTemplate(dataSource); 8 var updatedCount = jdbcTemplate.update(sql, new PreparedStatementSetter() { 9 @Override 10 public void setValues(PreparedStatement preparedStatement) throws SQLException { 11 preparedStatement.setString(1, person.getName()); 12 preparedStatement.setString(2, person.getGender()); 13 } 14 }); 15 return updatedCount; 16 }
哇,代碼一下就減少了,而且減少不止一點!以前,我們進行數據插入時,總要經過以下十步:
01.調用getDataSource方法創建數據源對象。
02.通過數據源對象獲得數據庫連接對象。
03.通過數據庫連接對象獲得PreparedStatement對象。
04.調用PreparedStatement對象的方法設置名字,性別兩個SQL參數。
05.執行SQL語句插入人的信息。
06.處理SQL語句執行異常。
07.關閉PreparedStatement
08.處理PreparedStatement關閉異常。
09.關閉數據庫連接。
10.處理數據庫連接關閉異常。
如今不用了,只需三步即可搞定:
1.調用getDataSource方法創建數據源對象。
2.以數據源對象作為參數創建JdbcTemplate對象。
3.調用JdbcTemplate對象的update方法執行SQL語句,插入人的信息。
超級簡單,對不對?於是問題來了:“通過兩種實現方式的強烈對比,我們深刻體會到了Spring JDBC對JDBC的簡化。可是,JdbcTemplate是什么呀?Spring JDBC封裝JDBC之后產生的一個類嗎?我們應該怎樣用它?”帶着這些問題,懷揣着美好的求知欲,我們踏上征途,上下求索。正如大家想的那樣,JdbcTemplate就是Spring JDBC封裝JDBC之后產生的一個類,而且是一個非常重要的核心類。該類定義了一些方法,以供我們調用之后執行任何我們想要執行的SQL語句,完成任何我們想做的數據庫操作。比如創建數據庫表,添加數據庫索引,增刪改查數據,等等。前文實現的addPerson方法正是通過調用JdbcTempdate的update方法實現人的信息的插入的。update方法的簽名如下:
public int update(String sql, PreparedStatementSetter pss)
update方法接受兩個參數。第一個參數是String類型的,用於指定即將執行的SQL語句;第二個參數是PreparedStatementSetter類型的,用於指定PreparedStatementSetter類型的對象,設置SQL參數。addPerson方法調用update方法時,指定了這條SQL語句:
1 var sql = " INSERT INTO person" 2 + " (person_name, person_gender)" 3 + " VALUES" 4 + " (?, ?)";
這條SQL語句具有兩個參數占位符,分別對應即將插入的person_name,person_gender。這意味着執行這條SQL語句之前,需先設好這些SQL參數。問題在於,這些SQL參數是怎樣設置的呢?
回答這個問題之前,最好先來瞧瞧update方法執行SQL語句的過程。大概如下:
1.通過數據源對象獲得數據庫連接對象。
2.通過數據庫連接對象獲得PreparedStatement對象。
3.以PreparedStatement對象作為參數,調用PreparedStatementSetter接口的setValues方法設置SQL參數。
4.執行SQL語句插入數據。
5.關閉PreparedStatement,關閉數據庫連接,處理異常。
看到了吧?問題的關鍵在於第三步。而這,與PreparedStatementSetter接口息息相關。PreparedStatementSetter接口定義如下:
1 @FunctionalInterface 2 public interface PreparedStatementSetter { 3 void setValues(PreparedStatement ps) throws SQLException; 4 }
這是一個函數式接口,專門用來設置SQL參數。因此,addPerson方法調用update方法時,以匿名類實現了PreparedStatementSetter接口,並作為第二個參數傳了進去。如下代碼片段所示:
1 var updatedCount = jdbcTemplate.update(sql, new PreparedStatementSetter() { 2 @Override 3 public void setValues(PreparedStatement preparedStatement) throws SQLException { 4 preparedStatement.setString(1, person.getName()); 5 preparedStatement.setString(2, person.getGender()); 6 } 7 });
看到了吧?在實現了PreparedStatementSetter接口的匿名類里,setValues方法設置了兩個SQL參數。這樣,update方法調用這個匿名類的setValues方法之后,就能正確設置SQL參數,從而順利執行SQL語句了。SQL語句執行完成之后,update方法將返回受影響的行數,也就是成功插入數據庫的數目。
還有,PreparedStatementSetter是一個函數式接口,自然也能使用lambda表達式進行實現。而且,如果使用lambda表達式進行實現的話,addPerson方法將會更加簡潔。大家不妨動手試試。另外,為了方便大家執行無需任何參數的SQL語句,Spring還提供了以下這種重載形式的update方法:
public int update(String sql)
這個方法非常簡單,傳入一條SQL語句即可執行。大家一看就知道怎么用了,這里不作介紹。卻該好好聊聊update方法能做的另外兩件重要的事情。興許大家已經猜到,除了插入之外,update方法還特別善長數據的刪除和修改。調用update方法進行數據的刪除和修改與調用update方法進行數據的插入並無分別,只是執行的SQL語句不同而已。這里不作太多介紹,只向大家示例兩個方法,大家看了之后自然知道怎么使用。這兩個方法一個名為deletePersonByName,一個名為updatePersonByName。deletePersonByName方法用於刪除名字等於某值的人的信息;updatePersonByName方法用於把人的名字改成新的名字。代碼如下:
public int deletePersonByName(String personName) { var sql = "DELETE FROM person WHERE person_name = ?"; var dataSource = this.getDataSource(); var jdbcTemplate = new JdbcTemplate(dataSource); var updatedCount = jdbcTemplate.update(sql, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement preparedStatement) throws SQLException { preparedStatement.setString(1, personName); } }); return updatedCount; } public int updatePersonByName(String personName, String newPersonName) { var sql = "UPDATE person SET person_name = ? WHERE person_name = ?"; var dataSource = this.getDataSource(); var jdbcTemplate = new JdbcTemplate(dataSource); var updatedCount = jdbcTemplate.update(sql, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement preparedStatement) throws SQLException { preparedStatement.setString(1, newPersonName); preparedStatement.setString(2, personName); } }); return updatedCount; }
增刪改既已談過,也就是時候聊聊查詢那些事了。不知大家可還記得,前文曾經寫過queryPersonByName方法。當時,這個方法是用JDBC實現的,能夠根據人的名字查出人的信息。現在,讓我們看看這個方法能用JdbcTemplate怎么實現。如下所示:
1 public List<Person> queryPersonByName(String personName) { 2 var sql = " SELECT" 3 + " person_id, person_name, person_gender" 4 + " FROM" 5 + " person" 6 + " WHERE" 7 + " person_name = ?"; 8 var dataSource = this.getDataSource(); 9 var jdbcTemplate = new JdbcTemplate(dataSource); 10 var personList = jdbcTemplate.query(sql, new PreparedStatementSetter() { 11 @Override 12 public void setValues(PreparedStatement preparedStatement) throws SQLException { 13 preparedStatement.setString(1, personName); 14 } 15 }, new RowMapper<Person>() { 16 @Override 17 public Person mapRow(ResultSet resultSet, int i) throws SQLException { 18 var person = new Person(); 19 person.setId(resultSet.getInt(1)); 20 person.setName(resultSet.getString(2)); 21 person.setGender(resultSet.getString(3)); 22 return person; 23 } 24 }); 25 return personList; 26 }
可以看到,這里調用了JdbcTemplate的query方法。方法簽名如下:
public <T> List<T> query(String sql, PreparedStatementSetter pss, RowMapper<T> rowMapper)
這個方法接受三個參數。前兩個參數和update方法一樣,用於傳入即將執行的SQL語句和通過實現PreparedStatementSetter接口設置SQL參數。第三個參數則不同,是支持泛型的RowMapper<T>接口。重點在於,這個接口是干嘛用的?
回答這個問題之前,最好先來瞧瞧query方法執行SQL語句的過程。大概如下:
01.通過數據源對象獲得數據庫連接對象。
02.通過數據庫連接對象獲得PreparedStatement對象。
03.以PreparedStatement對象作為參數,調用PreparedStatementSetter接口的setValues方法設置SQL參數。
04.執行SQL語句查詢數據,得到ResultSet對象。
05.創建一個空的結果列表。
06.循環調用ResultSet對象的next()方法,遍歷查詢結果。每遍歷一條查詢結果,就以遍歷到的ResultSet對象和當前對象的索引(也就是遍歷到的第幾條數據)作為參數,調用一次RowMapper<T>接口的mapRow方法。
07.mapRow方法處理查詢結果之后,返回一個對象。這個對象的類型,就是我們實現RowMapper<T>接口時指定的類型。
08.把調用mapRow方法返回的對象存進結果列表。
09.如此循環遍歷完成之后,關閉PreparedStatement,關閉數據庫連接,處理異常。
10.返回結果列表。
看到了吧?問題的關鍵在於第六步和第七步。而這,與RowMapper<T>接口息息相關。RowMapper<T>接口定義如下:
@FunctionalInterface public interface RowMapper<T> { @Nullable T mapRow(ResultSet rs, int rowNum) throws SQLException; }
不出大家所料,RowMapper<T>也是一個函數式接口,而且是一個支持泛型的函數式接口。專門用來處理query方法每次遍歷到的查詢結果。因此,queryPersonByName方法調用query方法時,以匿名類實現了RowMapper<T>接口,並作為第三個參數傳了進去。如下代碼片段所示:
1 var personList = jdbcTemplate.query(sql, new PreparedStatementSetter() { 2 @Override 3 public void setValues(PreparedStatement preparedStatement) throws SQLException { 4 preparedStatement.setString(1, personName); 5 } 6 }, new RowMapper<Person>() { 7 @Override 8 public Person mapRow(ResultSet resultSet, int i) throws SQLException { 9 var person = new Person(); 10 person.setId(resultSet.getInt(1)); 11 person.setName(resultSet.getString(2)); 12 person.setGender(resultSet.getString(3)); 13 return person; 14 } 15 });
看到了吧?在實現了RowMapper<T>接口的匿名類里,mapRow方法創建了Person對象,並從ResultSet對象里拿到數據之后進行填充。這樣,query方法每遍歷到一個ResultSet查詢結果,就調用一次匿名類的mapRow方法。mapRow方法處理ResultSet查詢結果之后,返回一個Person類型的對象,這個對象被query方法存進Person類型的結果列表里。待到所有結果遍歷完成之后,query方法就將Person類型的結果列表返回。於是,調用query方法之后,我們能夠順利拿到Person類型的結果列表。另外,為了方便大家執行無需任何參數的SQL語句,Spring還提供了這種重載的query方法:
public <T> List<T> query(String sql, RowMapper<T> rowMapper)
這種重載除了缺少PreparedStatementSetter類型的參數之外,其它都是一樣的。大家一看就知道怎么用了,這里不作介紹。卻該好好聊聊另外一種常見的查詢。很多時候,我們想查的數據並非列表形式的;而是一個整數,一個字符串,一個對象,等等。比如,這個方法執行之后,就只返回一個Person對象:
1 public Person queryPersonById(int id) { 2 var sql = " SELECT" 3 + " person_id, person_name, person_gender" 4 + " FROM" 5 + " person" 6 + " WHERE" 7 + " person_id = ?"; 8 var dataSource = this.getDataSource(); 9 var jdbcTemplate = new JdbcTemplate(dataSource); 10 var person = jdbcTemplate.query(sql, new PreparedStatementSetter() { 11 @Override 12 public void setValues(PreparedStatement preparedStatement) throws SQLException { 13 preparedStatement.setInt(1, id); 14 } 15 }, new ResultSetExtractor<Person>() { 16 @Override 17 public Person extractData(ResultSet resultSet) throws SQLException, DataAccessException { 18 if (resultSet.next()) { 19 var person = new Person(); 20 person.setId(resultSet.getInt(1)); 21 person.setName(resultSet.getString(2)); 22 person.setGender(resultSet.getString(3)); 23 return person; 24 } else { 25 return null; 26 } 27 } 28 }); 29 return person; 30 }
這里調用了query方法的另外一種重載。方法簽名如下:
public <T> T query(String sql, PreparedStatementSetter pss, ResultSetExtractor<T> rse)
這種重載的query方法同樣接受三個參數。前兩個參數是一樣的。第三個參數則不同,是支持泛型的ResultSetExtractor<T>接口。重點同樣在於,這個接口是干嘛用的?
回答這個問題之前,最好先來瞧瞧query方法執行SQL語句的過程。大概如下:
1.通過數據源對象獲得數據庫連接對象。
2.通過數據庫連接對象獲得PreparedStatement對象。
3.以PreparedStatement對象作為參數,調用PreparedStatementSetter接口的setValues方法設置SQL參數。
4.執行SQL語句查詢數據,得到ResultSet對象。
5.以ResultSet對象作為參數,直接調用ResultSetExtractor<T>接口的extractData方法進行查詢結果的處理。
6.關閉PreparedStatement,關閉數據庫連接,處理異常。
7.返回extractData方法處理之后的結果。
看到了吧?問題的關鍵在於第五步。而這,與ResultSetExtractor<T>接口息息相關。ResultSetExtractor<T>接口定義如下:
1 @FunctionalInterface 2 public interface ResultSetExtractor<T> { 3 @Nullable 4 T extractData(ResultSet rs) throws SQLException, DataAccessException; 5 }
和大家想的一樣,ResultSetExtractor<T>同樣也是一個函數式接口,而且是一個支持泛型的函數式接口。專門用來處理查詢結果。因此,queryPersonById方法調用query方法時,以匿名類實現了ResultSetExtractor<T>接口,並作為第三個參數傳了進去。如下代碼片段所示:
1 var person = jdbcTemplate.query(sql, new PreparedStatementSetter() { 2 @Override 3 public void setValues(PreparedStatement preparedStatement) throws SQLException { 4 preparedStatement.setInt(1, id); 5 } 6 }, new ResultSetExtractor<Person>() { 7 @Override 8 public Person extractData(ResultSet resultSet) throws SQLException, DataAccessException { 9 if (resultSet.next()) { 10 var person = new Person(); 11 person.setId(resultSet.getInt(1)); 12 person.setName(resultSet.getString(2)); 13 person.setGender(resultSet.getString(3)); 14 return person; 15 } else { 16 return null; 17 } 18 } 19 });
看到了吧?在實現了ResultSetExtractor<T>接口的匿名類里,extractData方法調用ResultSet對象的next()方法獲取查詢結果,之后創建Person對象,把ResultSet的數據填充進去,返回Person對象。同樣的,為了方便大家執行無需任何參數的SQL語句,Spring還提供了這種重載的query方法:
public <T> T query(String sql, ResultSetExtractor<T> rse)
現在,我們已經知道怎樣使用Spring JDBC進行增刪改查了。增刪改查只是Spring JDBC提供的基本功能,還有很多東西沒有討論。比如如何進行異常處理,如何執行存儲過程,如何執行事務,等等。我們將在“細說Spring JDBC”的時候進行介紹。現在,我們只需知道怎樣使用Spring JDBC進行增刪改查就行。卻從下章開始先來瞧瞧Spring關於簡化Web開發的那些趣事。歡迎大家繼續閱讀,謝謝大家!