從今天開始,我們將進(jìn)入 Spring Boot 另一個(gè)核心技術(shù)體系的討論,即數(shù)據(jù)訪問技術(shù)體系。無論是互聯(lián)網(wǎng)應(yīng)用還是傳統(tǒng)軟件,對(duì)于任何一個(gè)系統(tǒng)而言,數(shù)據(jù)的存儲(chǔ)和訪問都是不可缺少的。
數(shù)據(jù)訪問層的構(gòu)建可能會(huì)涉及多種不同形式的數(shù)據(jù)存儲(chǔ)媒介,本課程關(guān)注的是最基礎(chǔ)也是最常用的數(shù)據(jù)存儲(chǔ)媒介,即關(guān)系型數(shù)據(jù)庫(kù),針對(duì)關(guān)系型數(shù)據(jù)庫(kù),Java 中應(yīng)用最廣泛的就是 JDBC 規(guī)范,今天我們將對(duì)這個(gè)經(jīng)典規(guī)范展開討論。
JDBC 是 Java Database Connectivity 的全稱,它的設(shè)計(jì)初衷是提供一套能夠應(yīng)用于各種數(shù)據(jù)庫(kù)的統(tǒng)一標(biāo)準(zhǔn),這套標(biāo)準(zhǔn)需要不同數(shù)據(jù)庫(kù)廠家之間共同遵守,并提供各自的實(shí)現(xiàn)方案供 JDBC 應(yīng)用程序調(diào)用。
作為一套統(tǒng)一標(biāo)準(zhǔn),JDBC 規(guī)范具備完整的架構(gòu)體系,如下圖所示:
JDBC 規(guī)范整體架構(gòu)圖
從上圖中可以看到,Java 應(yīng)用程序通過 JDBC 所提供的 API 進(jìn)行數(shù)據(jù)訪問,而這些 API 中包含了開發(fā)人員所需要掌握的各個(gè)核心編程對(duì)象,下面我們一起來看下。
對(duì)于日常開發(fā)而言,JDBC 規(guī)范中的核心編程對(duì)象包括 DriverManger、DataSource、Connection、Statement,及 ResultSet。
正如前面的 JDBC 規(guī)范整體架構(gòu)圖中所示,JDBC 中的 DriverManager 主要負(fù)責(zé)加載各種不同的驅(qū)動(dòng)程序(Driver),并根據(jù)不同的請(qǐng)求向應(yīng)用程序返回相應(yīng)的數(shù)據(jù)庫(kù)連接(Connection),應(yīng)用程序再通過調(diào)用 JDBC API 實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的操作。
JDBC 中的 Driver 定義如下,其中最重要的是第一個(gè)獲取 Connection 的 connect 方法:
- public interface Driver {
- //獲取數(shù)據(jù)庫(kù)連接
- Connection connect(String url, java.util.Properties info)
- throws SQLException;
- boolean acceptsURL(String url) throws SQLException;
- DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
- throws SQLException;
- int getMajorVersion();
- int getMinorVersion();
- boolean jdbcCompliant();
- public Logger getParentLogger() throws SQLFeatureNotSupportedException;
- }
針對(duì) Driver 接口,不同的數(shù)據(jù)庫(kù)供應(yīng)商分別提供了自身的實(shí)現(xiàn)方案。例如,MySQL 中的 Driver 實(shí)現(xiàn)類如下代碼所示:
- public class Driver extends NonRegisteringDriver implements java.sql.Driver {
- // 通過 DriverManager 注冊(cè) Driver
- static {
- try {
- java.sql.DriverManager.registerDriver(new Driver());
- } catch (SQLException E) {
- throw new RuntimeException("Can't register driver!");
- }
- }
- …
- }
這里就使用用了 DriverManager,而 DriverManager 除提供了上述用于注冊(cè) Driver 的 registerDriver 方法之外,還提供了 getConnection 方法用于針對(duì)具體的 Driver 獲取 Connection 對(duì)象。
通過前面的介紹,我們知道在 JDBC 規(guī)范中可直接通過 DriverManager 獲取 Connection,我們也知道獲取 Connection 的過程需要建立與數(shù)據(jù)庫(kù)之間的連接,而這個(gè)過程會(huì)產(chǎn)生較大的系統(tǒng)開銷。
為了提高性能,通常我們首先會(huì)建立一個(gè)中間層將 DriverManager 生成的 Connection 存放到連接池中,再?gòu)某刂蝎@取 Connection。
而我們可以認(rèn)為 DataSource 就是這樣一個(gè)中間層,它作為 DriverManager 的替代品而推出,是獲取數(shù)據(jù)庫(kù)連接的首選方法。
DataSource 在 JDBC 規(guī)范中代表的是一種數(shù)據(jù)源,核心作用是獲取數(shù)據(jù)庫(kù)連接對(duì)象 Connection。在日常開發(fā)過程中,我們通常會(huì)基于 DataSource 獲取 Connection。DataSource 接口的定義如下代碼所示:
- public interface DataSource extends CommonDataSource, Wrapper {
- Connection getConnection() throws SQLException;
- Connection getConnection(String username, String password)
- throws SQLException;
- }
從上面我們可以看到,DataSource 接口提供了兩個(gè)獲取 Connection 的重載方法,并繼承了 CommonDataSource 接口。CommonDataSource 是 JDBC 中關(guān)于數(shù)據(jù)源定義的根接口,除了 DataSource 接口之外,它還有另外兩個(gè)子接口,如下圖所示:
DataSource 類層結(jié)構(gòu)圖
其中,DataSource 是官方定義的獲取 Connection 的基礎(chǔ)接口,XADataSource 用來在分布式事務(wù)環(huán)境下實(shí)現(xiàn) Connection 的獲取,而 ConnectionPoolDataSource 是從連接池 ConnectionPool 中獲取 Connection 的接口。
所謂的 ConnectionPool 相當(dāng)于預(yù)先生成一批 Connection 并存放在池中,從而提升 Connection 獲取的效率。
請(qǐng)注意 DataSource 接口同時(shí)還繼承了一個(gè) Wrapper 接口。從接口的命名上看,我們可以判斷該接口起到一種包裝器的作用。事實(shí)上,因?yàn)楹芏鄶?shù)據(jù)庫(kù)供應(yīng)商提供了超越標(biāo)準(zhǔn) JDBC API 的擴(kuò)展功能,所以 Wrapper 接口可以把一個(gè)由第三方供應(yīng)商提供的、非 JDBC 標(biāo)準(zhǔn)的接口包裝成標(biāo)準(zhǔn)接口。
以 DataSource 接口為例,如果我們想自己實(shí)現(xiàn)一個(gè)定制化的數(shù)據(jù)源類 MyDataSource,就可以提供一個(gè)實(shí)現(xiàn)了 Wrapper 接口的 MyDataSourceWrapper 類來完成包裝和適配,如下圖所示:
通過 Wrapper 接口擴(kuò)展 JDBC 規(guī)范示意圖
在 JDBC 規(guī)范中,除了 DataSource 之外,Connection、Statement、ResultSet 等核心對(duì)象也都繼承了這個(gè) Wrapper 接口。
作為一種基礎(chǔ)組件,它同樣不需要開發(fā)人員自己實(shí)現(xiàn) DataSource,因?yàn)闃I(yè)界已經(jīng)存在了很多優(yōu)秀的實(shí)現(xiàn)方案,如 DBCP、C3P0 和 Druid 等。
例如 Druid 提供了 DruidDataSource,它不僅提供了連接池的功能,還提供了諸如監(jiān)控等其他功能,它的類層結(jié)構(gòu)如下圖所示:
DruidDataSource 的類層結(jié)構(gòu)
DataSource 的目的是獲取 Connection 對(duì)象。我們可以把 Connection 理解為一種會(huì)話(Session)機(jī)制,Connection 代表一個(gè)數(shù)據(jù)庫(kù)連接,負(fù)責(zé)完成與數(shù)據(jù)庫(kù)之間的通信。
所有 SQL 的執(zhí)行都是在某個(gè)特定 Connection 環(huán)境中進(jìn)行的,同時(shí)它還提供了一組重載方法分別用于創(chuàng)建 Statement 和 PreparedStatement。另一方面,Connection 也涉及事務(wù)相關(guān)的操作。
Connection 接口中定義的方法很豐富,其中最核心的幾個(gè)方法如下代碼所示:
- public interface Connection extends Wrapper, AutoCloseable {
- //創(chuàng)建 Statement
- Statement createStatement() throws SQLException;
- //創(chuàng)建 PreparedStatement
- PreparedStatement prepareStatement(String sql) throws SQLException;
- //提交
- void commit() throws SQLException;
- //回滾
- void rollback() throws SQLException;
- //關(guān)閉連接
- void close() throws SQLException;
- }
這里涉及具體負(fù)責(zé)執(zhí)行 SQL 語句的 Statement 和 PreparedStatement 對(duì)象,我們接著往下看。
JDBC 規(guī)范中的 Statement 存在兩種類型,一種是普通的 Statement,一種是支持預(yù)編譯的 PreparedStatement。
所謂預(yù)編譯,是指數(shù)據(jù)庫(kù)的編譯器會(huì)對(duì) SQL 語句提前編譯,然后將預(yù)編譯的結(jié)果緩存到數(shù)據(jù)庫(kù)中,下次執(zhí)行時(shí)就可以通過替換參數(shù)并直接使用編譯過的語句,從而大大提高 SQL 的執(zhí)行效率。
當(dāng)然,這種預(yù)編譯也需要一定成本,因此在日常開發(fā)中,如果對(duì)數(shù)據(jù)庫(kù)只執(zhí)行一次性讀寫操作時(shí),用 Statement 對(duì)象進(jìn)行處理會(huì)比較合適;而涉及 SQL 語句的多次執(zhí)行時(shí),我們可以使用 PreparedStatement。
如果需要查詢數(shù)據(jù)庫(kù)中的數(shù)據(jù),我們只需要調(diào)用 Statement 或 PreparedStatement 對(duì)象的 executeQuery 方法即可。
這個(gè)方法以 SQL 語句作為參數(shù),執(zhí)行完后返回一個(gè) JDBC 的 ResultSet 對(duì)象。當(dāng)然,Statement 或 PreparedStatement 還提供了一大批執(zhí)行 SQL 更新和查詢的重載方法,我們無意一一展開。
以 Statement 為例,它的核心方法如下代碼所示:
- public interface Statement extends Wrapper, AutoCloseable {
- //執(zhí)行查詢語句
- ResultSet executeQuery(String sql) throws SQLException;
- //執(zhí)行更新語句
- int executeUpdate(String sql) throws SQLException;
- //執(zhí)行 SQL 語句
- boolean execute(String sql) throws SQLException;
- //執(zhí)行批處理
- int[] executeBatch() throws SQLException;
- }
這里我們同樣引出了 JDBC 規(guī)范中最后一個(gè)核心編程對(duì)象,即代表執(zhí)行結(jié)果的 ResultSet。
一旦我們通過 Statement 或 PreparedStatement 執(zhí)行了 SQL 語句并獲得了 ResultSet 對(duì)象,就可以使用該對(duì)象中定義的一大批用于獲取 SQL 執(zhí)行結(jié)果值的工具方法,如下代碼所示:
- public interface ResultSet extends Wrapper, AutoCloseable {
- //獲取下一個(gè)結(jié)果
- boolean next() throws SQLException;
- //獲取某一個(gè)類型的結(jié)果值
- Value getXXX(int columnIndex) throws SQLException;
- …
- }
ResultSet 提供了 next() 方法便于開發(fā)人員實(shí)現(xiàn)對(duì)整個(gè)結(jié)果集的遍歷。如果 next() 方法返回為 true,意味著結(jié)果集中存在數(shù)據(jù),可以調(diào)用 ResultSet 對(duì)象的一系列 getXXX() 方法來取得對(duì)應(yīng)的結(jié)果值。
對(duì)于開發(fā)人員而言,JDBC API 是我們?cè)L問數(shù)據(jù)庫(kù)的主要途徑,如果我們使用 JDBC 開發(fā)一個(gè)訪問數(shù)據(jù)庫(kù)的執(zhí)行流程,常見的代碼風(fēng)格如下所示(省略了異常處理):
- // 創(chuàng)建池化的數(shù)據(jù)源
- PooledDataSource dataSource = new PooledDataSource ();
- // 設(shè)置 MySQL Driver
- dataSource.setDriver ("com.mysql.jdbc.Driver");
- // 設(shè)置數(shù)據(jù)庫(kù) URL、用戶名和密碼
- dataSource.setUrl ("jdbc:mysql://localhost:3306/test");
- dataSource.setUsername("root");
- dataSource.setPassword("root");
- // 獲取連接
- Connection connection = dataSource.getConnection();
- // 執(zhí)行查詢
- PreparedStatement statement = connection.prepareStatement ("select * from user");
- // 獲取查詢結(jié)果進(jìn)行處理
- ResultSet resultSet = statement.executeQuery();
- while (resultSet.next()) {
- …
- }
- // 關(guān)閉資源
- statement.close();
- resultSet.close();
- connection.close();
這段代碼中完成了對(duì)基于前面介紹的 JDBC API 中的各個(gè)核心編程對(duì)象的數(shù)據(jù)訪問。上述代碼主要面向查詢場(chǎng)景,而針對(duì)用于插入數(shù)據(jù)的處理場(chǎng)景,我們只需要在上述代碼中替換幾行代碼,即將“執(zhí)行查詢”和“獲取查詢結(jié)果進(jìn)行處理”部分的查詢操作代碼替換為插入操作代碼就行。
最后,我們梳理一下基于 JDBC 規(guī)范進(jìn)行數(shù)據(jù)庫(kù)訪問的整個(gè)開發(fā)流程,如下圖所示:
基于 JDBC 規(guī)范進(jìn)行數(shù)據(jù)庫(kù)訪問的開發(fā)流程圖
針對(duì)前面所介紹的代碼示例,我們明確地將基于 JDBC 規(guī)范訪問關(guān)系型數(shù)據(jù)庫(kù)的操作分成兩大部分:一部分是準(zhǔn)備和釋放資源以及執(zhí)行 SQL 語句,另一部分則是處理 SQL 執(zhí)行結(jié)果。
而對(duì)于任何數(shù)據(jù)訪問而言,前者實(shí)際上都是重復(fù)的。在上圖所示的整個(gè)開發(fā)流程中,事實(shí)上只有“處理 ResultSet ”部分的代碼需要開發(fā)人員根據(jù)具體的業(yè)務(wù)對(duì)象進(jìn)行定制化處理。這種抽象為整個(gè)執(zhí)行過程提供了優(yōu)化空間。諸如 Spring 框架中 JdbcTemplate 這樣的模板工具類就應(yīng)運(yùn)而生了,我們會(huì)在 07 講中會(huì)詳細(xì)介紹這個(gè)模板工具類。
JDBC 規(guī)范是 Java EE 領(lǐng)域中進(jìn)行數(shù)據(jù)庫(kù)訪問的標(biāo)準(zhǔn)規(guī)范,在業(yè)界應(yīng)用非常廣泛。今天的課程中,我們分析了該規(guī)范的核心編程對(duì)象,并梳理了使用 JDBC 規(guī)范訪問數(shù)據(jù)庫(kù)的開發(fā)流程。希望你能熟練掌握這部分知識(shí),因?yàn)槭炀氄莆?JDBC 規(guī)范是我們理解后續(xù)內(nèi)容的基礎(chǔ)。
這里給你留一道思考題:在使用 JDBC 規(guī)范時(shí),開發(fā)人員主要應(yīng)用哪些編程對(duì)象完成對(duì)數(shù)據(jù)庫(kù)的訪問?
盡管 JDBC 規(guī)范非常經(jīng)典,但其所提供的 API 過于面向底層,對(duì)于開發(fā)人員來說并不友好。因此 07 講中,我們將引入 Spring 框架中提供的 JdbcTemplate 模板工具類來簡(jiǎn)化 JDBC 規(guī)范的使用方法。
聯(lián)系客服