在現代世界,沒有資料儲存就沒有出路。使用資料庫的歷史始於很久以前,隨著 JDBC 的出現。我建議記住一些建立在 JDBC 之上的現代框架都離不開的東西。此外,即使與他們一起工作,有時你也可能需要「回歸本源」的機會。我希望這篇評論能夠作為介紹或幫助您刷新記憶。
此標準由規範「 JSR 221 JDBC 4.1 API 」描述。這個規格告訴我們,JDBC API 提供了從 Java 編寫的程式對關聯式資料庫的程式存取。它還表明 JDBC API 是 Java 平台的一部分,因此包含在 Java SE 和 Java EE 中。JDBC API 在兩個套件中提供:java.sql 和 javax.sql。那我們就來認識他們吧。
正如我們所看到的,這是合乎邏輯的,資料庫是一個外部元件,不是 Java SE 原生的。解釋起來很簡單——資料庫數量巨大,您可以使用任何一個。例如,有PostgreSQL、Oracle、MySQL、H2。這些資料庫中的每一個都由稱為資料庫供應商的獨立公司提供。每個資料庫都是用自己的程式語言(不一定是 Java)編寫的。為了能夠從 Java 應用程式使用資料庫,資料庫提供者編寫了一個特殊的驅動程序,它是它自己的圖像適配器。這種相容於 JDBC 的資料庫(即具有 JDBC 驅動程式的資料庫)也稱為「JDBC 相容資料庫」。這裡我們可以用電腦設備來類比。例如,在記事本中有一個「列印」按鈕。每次按下它時,程式都會告訴作業系統記事本應用程式想要列印。而且你有一台印表機。要教導您的作業系統與 Canon 或 HP 印表機統一通信,您將需要不同的驅動程式。但對於身為使用者的您來說,什麼都不會改變。您仍然會按下同一個按鈕。與 JDBC 相同。您正在運行相同的程式碼,只是不同的資料庫可能在背景運行。我認為這是一個非常明確的方法。每個這樣的 JDBC 驅動程式都是某種工件、函式庫、jar 檔案。這是我們專案的依賴項。例如,我們可以選擇資料庫“ H2 Database ”,然後我們需要新增以下依賴項:
介紹
程式語言的主要目的之一是儲存和處理資訊。為了更好地理解資料儲存的工作原理,值得花一些時間來了解應用程式的理論和架構。例如,您可以閱讀文獻,即 Joseph Ingeno 所寫的《軟體架構師手冊:透過實施有效的架構成為一名成功的軟體架構師...》一書。如前所述,存在一定的資料層或「資料層」。它包括儲存資料的位置(例如,SQL 資料庫)和用於處理資料儲存的工具(例如,我們將討論的 JDBC)。微軟網站上還有一篇文章:《設計基礎架構持久層》,描述了從資料層中分離出一個附加層-持久層的架構解決方案。在這種情況下,資料層是資料本身的儲存級別,而持久層是用於處理來自資料層級別的儲存的資料的某種抽象級別。持久層可以包括“DAO”模板或各種 ORM。但 ORM 是另一個討論的議題。您可能已經了解,資料層首先出現。從 JDK 1.1 開始,Java 世界就出現了 JDBC(Java DataBase Connectivity - Java 中的資料庫連線)。這是 Java 應用程式與各種 DBMS 互動的標準,以 Java SE 中包含的 java.sql 和 javax.sql 套件的形式實現:![JDBC 或一切的開始 - 2](https://cdn.javarush.com/images/article/44cc9e7c-7cc4-4300-a112-3eff003d1ec4/1024.jpeg)
![JDBC 或一切的開始 - 3](https://cdn.javarush.com/images/article/2bd75bae-b172-4253-8267-7e92c84b7578/original.jpeg)
工作開始
要了解 JDBC API 的一般意義,我們需要一個 Java 應用程式。使用項目組裝系統之一是最方便的。例如,讓我們使用Gradle。您可以在簡短的評論中閱讀有關 Gradle 的更多資訊:「Gradle 簡介」。首先,讓我們初始化一個新的 Gradle 專案。由於Gradle的功能是透過插件實現的,所以我們需要使用「Gradle Build Init Plugin」來初始化:gradle init --type java-application
之後,讓我們打開建置腳本 - build.gradle文件,它描述了我們的專案以及如何使用它。我們對「依賴項」區塊感興趣,其中描述了依賴項 - 即那些庫/框架/api,沒有它們我們就無法工作並且依賴它們。預設情況下我們會看到類似以下內容:
dependencies {
// This dependency is found on compile classpath of this component and consumers.
implementation 'com.google.guava:guava:26.0-jre'
// Use JUnit test framework
testImplementation 'junit:junit:4.12'
}
為什麼我們會在這裡看到這個?這些是我們專案的依賴項,是我們創建專案時Gradle自動產生的。也因為 guava 是一個獨立的函式庫,不包含在 Java SE 中。JUnit 也不包含在 Java SE 中。但我們有開箱即用的 JDBC,也就是說,它是 Java SE 的一部分。事實證明我們有 JDBC。偉大的。我們還需要什麼?有這樣一個精彩的圖表:
![JDBC 或一切的開始 - 4](https://cdn.javarush.com/images/article/580edb2f-3ef8-4a06-abcc-54f9321cba8b/1024.jpeg)
dependencies {
implementation 'com.h2database:h2:1.4.197'
如何找到依賴項以及如何描述它在資料庫提供者的官方網站或「Maven Central」上都有說明。如您所知,JDBC 驅動程式不是資料庫。但他只是一個嚮導。但是有一種叫做「記憶體資料庫」的東西。這些資料庫在應用程式的整個生命週期內都存在於記憶體中。通常,這通常用於測試或培訓目的。這使您可以避免在電腦上安裝單獨的資料庫伺服器。這非常適合我們熟悉JDBC。我們的沙箱已準備就緒,我們可以開始了。
![JDBC 或一切的開始 - 5](https://cdn.javarush.com/images/article/9a5233ae-4a8d-4cff-a6b4-b96ff5cc65ae/1024.jpeg)
聯繫
所以,我們有一個 JDBC 驅動程序,我們有一個 JDBC API。我們記得,JDBC 代表 Java 資料庫連線。因此,這一切都始於連結性——建立連結的能力。連接就是連接。讓我們再次翻到JDBC 規範的正文並查看目錄。在「 CHAPTER 4 Overview 」(概述)一章中我們翻到「4.1Establishing a Connection」(建立連接)一節,據說有兩種方式連接資料庫:- 透過驅動管理器
- 透過資料來源
Connection con = DriverManager.getConnection(url, user, passwd);
這些參數可以從我們選擇的資料庫的網站上取得。在我們的例子中,這是 H2 - “ H2 Cheat Sheet ”。讓我們繼續看 Gradle 準備的 AppTest 類別。它包含 JUnit 測試。JUnit 測試是一種以註解標記的方法@Test
。單元測試不是本次審查的主題,因此我們將簡單地限制自己的理解,即這些是以某種方式描述的方法,其目的是測試某些東西。根據 JDBC 規範和 H2 網站,我們將檢查是否已收到與資料庫的連線。我們來寫一個獲取連結的方法:
private Connection getNewConnection() throws SQLException {
String url = "jdbc:h2:mem:test";
String user = "sa";
String passwd = "sa";
return DriverManager.getConnection(url, user, passwd);
}
現在讓我們為此方法編寫一個測試,以檢查連接是否實際建立:
@Test
public void shouldGetJdbcConnection() throws SQLException {
try(Connection connection = getNewConnection()) {
assertTrue(connection.isValid(1));
assertFalse(connection.isClosed());
}
}
執行此測試時,將驗證產生的連線是否有效(正確建立)並且未關閉。透過使用try-with-resources,我們將在不再需要資源時釋放它們。這將保護我們免受連接鬆弛和記憶體洩漏的影響。由於與資料庫的任何操作都需要連接,因此讓我們在測試開始時提供標記為 @Test 的其餘測試方法並帶有 Connection,我們將在測試後發布。為此,我們需要兩個註釋:@Before 和 @After 讓我們在 AppTest 類別中添加一個新字段,用於儲存用於測試的 JDBC 連接:
private static Connection connection;
讓我們新增方法:
@Before
public void init() throws SQLException {
connection = getNewConnection();
}
@After
public void close() throws SQLException {
connection.close();
}
現在,任何測試方法都保證有一個 JDBC 連接,而不必每次都自行建立它。
![JDBC 或一切的開始 - 6](https://cdn.javarush.com/images/article/91db5db3-2308-4d78-ae35-aced4ef73f8c/original.jpeg)
聲明
接下來我們對語句或表達式感興趣。它們在「第 13 章聲明」一章的文檔中進行了描述。首先,它說有幾種類型或類型的語句:- 語句:不包含參數的SQL表達式
- PreparedStatement :包含輸入參數的已準備好的 SQL 語句
- CallableStatement:能夠從 SQL 預存程序取得傳回值的 SQL 表達式。
private int executeUpdate(String query) throws SQLException {
Statement statement = connection.createStatement();
// Для Insert, Update, Delete
int result = statement.executeUpdate(query);
return result;
}
讓我們新增一個使用之前的方法建立測試表的方法:
private void createCustomerTable() throws SQLException {
String customerTableQuery = "CREATE TABLE customers " +
"(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)";
String customerEntryQuery = "INSERT INTO customers " +
"VALUES (73, 'Brian', 33)";
executeUpdate(customerTableQuery);
executeUpdate(customerEntryQuery);
}
現在讓我們測試一下:
@Test
public void shouldCreateCustomerTable() throws SQLException {
createCustomerTable();
connection.createStatement().execute("SELECT * FROM customers");
}
現在讓我們執行請求,甚至帶有參數:
@Test
public void shouldSelectData() throws SQLException {
createCustomerTable();
String query = "SELECT * FROM customers WHERE name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, "Brian");
boolean hasResult = statement.execute();
assertTrue(hasResult);
}
JDBC 不支援PreparedStatement 的命名參數,因此參數本身由問題指定,透過指定值我們指示問題索引(從1 開始,而不是0)。在上次測試中,我們收到 true 作為是否有結果的指示。但是查詢結果在 JDBC API 中是如何表示的呢?它以結果集的形式呈現。
![JDBC 或一切的開始 - 7](https://cdn.javarush.com/images/article/8fb69684-bfcd-4d0e-a2b8-25ea9d0a709c/original.jpeg)
結果集
ResultSet 的概念在 JDBC API 規範的「第 15 章結果集」一章中進行了描述。首先,它說 ResultSet 提供了檢索和操作執行查詢結果的方法。也就是說,如果execute方法回傳true給我們,那麼我們就可以得到一個ResultSet。我們將對 createCustomerTable() 方法的呼叫移至 init 方法中,該方法被標記為 @Before。現在讓我們完成 shouldSelectData 測試:@Test
public void shouldSelectData() throws SQLException {
String query = "SELECT * FROM customers WHERE name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, "Brian");
boolean hasResult = statement.execute();
assertTrue(hasResult);
// Обработаем результат
ResultSet resultSet = statement.getResultSet();
resultSet.next();
int age = resultSet.getInt("age");
assertEquals(33, age);
}
這裡值得注意的是,next 是移動所謂「遊標」的方法。ResultSet 中的遊標指向某行。因此,為了讀取一行,您需要將遊標放在該行上。當遊標移動時,如果遊標有效(正確,正確),即指向數據,則遊標移動方法傳回true。如果傳回false,則表示沒有數據,即遊標沒有指向數據。如果我們嘗試使用無效遊標來取得數據,我們將收到錯誤:沒有可用資料。同樣有趣的是,透過 ResultSet 您可以更新甚至插入行:
@Test
public void shouldInsertInResultSet() throws SQLException {
Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet resultSet = statement.executeQuery("SELECT * FROM customers");
resultSet.moveToInsertRow();
resultSet.updateLong("id", 3L);
resultSet.updateString("name", "John");
resultSet.updateInt("age", 18);
resultSet.insertRow();
resultSet.moveToCurrentRow();
}
行集
除了ResultSet之外,JDBC還引入了RowSet的概念。您可以在此處閱讀更多內容:「JDBC 基礎知識:使用 RowSet 物件」。有多種用途。例如,最簡單的情況可能如下所示:@Test
public void shouldUseRowSet() throws SQLException {
JdbcRowSet jdbcRs = new JdbcRowSetImpl(connection);
jdbcRs.setCommand("SELECT * FROM customers");
jdbcRs.execute();
jdbcRs.next();
String name = jdbcRs.getString("name");
assertEquals("Brian", name);
}
正如你所看到的,RowSet類似於語句(我們透過它指定指令)和執行指令的共生體。透過它我們控制遊標(透過呼叫 next 方法)並從中獲取資料。這種方法不僅有趣,而且還有可能的實現方式。例如,CachedRowSet。它是「斷開連接」的(即,它不使用與資料庫的持久連接)並且需要與資料庫明確同步:
CachedRowSet jdbcRsCached = new CachedRowSetImpl();
jdbcRsCached.acceptChanges(connection);
您可以在 Oracle 網站上的教學中閱讀更多內容:「Using CachedRowSetObjects」。
![JDBC 或一切的開始 - 8](https://cdn.javarush.com/images/article/94060f13-dab0-4210-b9e5-370cc4c03bf6/1024.jpeg)
元數據
除了查詢之外,到資料庫的連線(即 Connection 類別的實例)還提供對元資料的存取 - 有關如何設定和組織資料庫的資料。但首先,我們提幾個關鍵點: 連接資料庫的 URL:「jdbc:h2:mem:test」。test 是我們資料庫的名稱。對於 JDBC API,這是一個目錄。並且名稱將是大寫的,即TEST。H2的預設模式是 PUBLIC。現在,讓我們來寫一個顯示所有使用者表的測試。為什麼要定制?因為資料庫不僅包含使用者表(我們自己使用create table表達式建立的表),還包含系統表。它們對於儲存有關資料庫結構的系統資訊是必要的。每個資料庫可以以不同的方式儲存此類系統表。例如,在 H2 中,它們儲存在「INFORMATION_SCHEMA」模式中。有趣的是,INFORMATION SCHEMA 是一種常見的方法,但 Oracle 走了一條不同的路線。您可以在這裡閱讀更多內容:「INFORMATION_SCHEMA 和 Oracle」。讓我們來寫一個接收用戶表元資料的測試:@Test
public void shoudGetMetadata() throws SQLException {
// У нас URL = "jdbc:h2:mem:test", где test - название БД
// Название БД = catalog
DatabaseMetaData metaData = connection.getMetaData();
ResultSet result = metaData.getTables("TEST", "PUBLIC", "%", null);
List<String> tables = new ArrayList<>();
while(result.next()) {
tables.add(result.getString(2) + "." + result.getString(3));
}
assertTrue(tables.contains("PUBLIC.CUSTOMERS"));
}
![JDBC 或一切的開始 - 9](https://cdn.javarush.com/images/article/783c4154-aa44-41f9-babf-64a35b76c027/1024.jpeg)
連接池
JDBC規範中的連線池有一個章節叫做「第11章連線池」。它還提供了需要連接池的主要理由。每個 Coonection 都是到資料庫的實體連接。它的創建和關閉是一項相當“昂貴”的工作。JDBC僅提供連線池API。因此,實施的選擇仍然是我們的。例如,此類實作包括HikariCP。因此,我們需要向專案依賴項新增一個池:dependencies {
implementation 'com.h2database:h2:1.4.197'
implementation 'com.zaxxer:HikariCP:3.3.1'
testImplementation 'junit:junit:4.12'
}
現在我們需要以某種方式使用這個池。為此,您需要初始化資料來源,也稱為資料來源:
private DataSource getDatasource() {
HikariConfig config = new HikariConfig();
config.setUsername("sa");
config.setPassword("sa");
config.setJdbcUrl("jdbc:h2:mem:test");
DataSource ds = new HikariDataSource(config);
return ds;
}
讓我們編寫一個測試來從池中接收連線:
@Test
public void shouldGetConnectionFromDataSource() throws SQLException {
DataSource datasource = getDatasource();
try (Connection con = datasource.getConnection()) {
assertTrue(con.isValid(1));
}
}
![JDBC 或一切的開始 - 10](https://cdn.javarush.com/images/article/3a5d6d7f-da70-4c4a-b3f4-6952f5bda7ba/1024.jpeg)
交易
JDBC 最有趣的事情之一就是交易。在 JDBC 規範中,它們被指派到「第 10 章事務」一章。首先,有必要了解什麼是交易。事務是一組對資料進行邏輯組合的順序操作,作為一個整體進行處理或取消。使用 JDBC 時交易何時開始?如規範所述,這是由 JDBC 驅動程式直接處理的。但通常情況下,噹噹前 SQL 語句需要事務且事務尚未建立時,新事務就會開始。交易什麼時候結束?這是由自動提交屬性控制的。如果啟用自動提交,則交易將在 SQL 語句「完成」後完成。「完成」的意思取決於 SQL 表達式的類型:- 資料操作語言,也稱為DML(插入、更新、刪除)
操作完成後事務就完成 Select 語句
- 傳回多個結果的 CallableStatement 和表達式
當所有關聯的 ResultSet 均已關閉且所有輸出均已收到(包括更新次數)時
當 ResultSet 關閉時交易完成(ResultSet#close)
@Test
public void shouldCommitTransaction() throws SQLException {
connection.setAutoCommit(false);
String query = "INSERT INTO customers VALUES (1, 'Max', 20)";
connection.createStatement().executeUpdate(query);
connection.commit();
Statement statement = connection.createStatement();
statement.execute("SELECT * FROM customers");
ResultSet resultSet = statement.getResultSet();
int count = 0;
while(resultSet.next()) {
count++;
}
assertEquals(2, count);
}
這很簡單。但只要我們只有一筆交易,這就是事實。有多個時該怎麼辦?他們需要彼此隔離。因此,我們來談談交易隔離等級以及 JDBC 如何處理它們。
![JDBC 或一切的開始 - 11](https://cdn.javarush.com/images/article/aca63374-0e45-4a69-be8b-27f1c52cbe4a/1024.jpeg)
絕緣等級
讓我們開啟 JDBC 規範的「10.2 交易隔離等級」小節。在這裡,在進一步討論之前,我想記住 ACID 這樣的事情。ACID 描述了事務系統的需求。- 原子性:
任何事務都不會部分提交給系統。要么執行其所有子操作,要么不執行任何子操作。 - 一致性:
根據定義,每筆成功的交易只記錄有效的結果。 - 隔離性:
交易運行時,並發事務不應影響其結果。 - 持久性:
如果交易成功完成,對其所做的變更不會因任何失敗而撤銷。
ext {
h2Version = '1.3.176' // 1.4.177
hikariVersion = '3.3.1'
junitVersion = '4.12'
}
接下來,我們在版本中使用它:
dependencies {
implementation "com.h2database:h2:${h2Version}"
implementation "com.zaxxer:HikariCP:${hikariVersion}"
testImplementation "junit:junit:${junitVersion}"
}
您可能已經注意到,h2 版本變低了。我們稍後會看到原因。那麼如何應用隔離等級呢?讓我們立即看一個實際的小例子:
@Test
public void shouldGetReadUncommited() throws SQLException {
Connection first = getNewConnection();
assertTrue(first.getMetaData().supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED));
first.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
first.setAutoCommit(false);
// Транзакиця на подключение. Поэтому первая транзакция с ReadUncommited вносит изменения
String insertQuery = "INSERT INTO customers VALUES (5, 'Max', 15)";
first.createStatement().executeUpdate(insertQuery);
// Вторая транзакция пытается их увидеть
int rowCount = 0;
JdbcRowSet jdbcRs = new JdbcRowSetImpl(getNewConnection());
jdbcRs.setCommand("SELECT * FROM customers");
jdbcRs.execute();
while (jdbcRs.next()) {
rowCount++;
}
assertEquals(2, rowCount);
}
有趣的是,此測試可能會在不支援 TRANSACTION_READ_UNCOMMITTED 的供應商(例如 sqlite 或 HSQL)上失敗。而且交易級別可能根本不起作用。還記得我們指出了 H2 資料庫驅動程式的版本嗎?如果我們將其提高到 h2Version = '1.4.177' 及更高版本,那麼 READ UNCOMMITTED 將停止工作,儘管我們沒有更改程式碼。這再次證明供應商和驅動程式版本的選擇不僅僅是字母,它實際上將決定您的請求將如何執行。您可以在此處閱讀有關如何在版本 1.4.177 中修復此行為以及它如何在更高版本中不起作用的信息:「在 MVStore 模式下支援 READ UNCOMMITTED 隔離等級」。
![JDBC 或一切的開始 - 12](https://cdn.javarush.com/images/article/6f38f710-4c47-47b0-8002-09b8cfe19cff/original.jpeg)
底線
正如我們所看到的,JDBC 是 Java 手中處理資料庫的強大工具。我希望這篇簡短的評論能為您提供一個起點或幫助您刷新記憶。好吧,作為零食,需要一些額外的材料:- 火災報告:“交易:神話、驚喜和機會”,馬丁·克萊普曼 (Martin Kleppmann)
- Yuri Tkach:“ JPA. 交易”
- Yurik Tkach:“ JDBC - 測試人員的 Java ”
- Udemy 上的免費課程:“ JDBC 和 MySQL ”
- “處理 CallableStatement 物件”
- IBM 開發人員:“ Java 資料庫連線”
- IBM 知識中心:“ JDBC 入門”
GO TO FULL VERSION