JavaRush /Java Blog /Random-TW /JDBC 或一切的開始
Viacheslav
等級 3

JDBC 或一切的開始

在 Random-TW 群組發布
在現代世界,沒有資料儲存就沒有出路。使用資料庫的歷史始於很久以前,隨著 JDBC 的出現。我建議記住一些建立在 JDBC 之上的現代框架都離不開的東西。此外,即使與他們一起工作,有時你也可能需要「回歸本源」的機會。我希望這篇評論能夠作為介紹或幫助您刷新記憶。
JDBC 或一切的開始 - 1

介紹

程式語言的主要目的之一是儲存和處理資訊。為了更好地理解資料儲存的工作原理,值得花一些時間來了解應用程式的理論和架構。例如,您可以閱讀文獻,即 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
此標準由規範「 JSR 221 JDBC 4.1 API 」描述。這個規格告訴我們,JDBC API 提供了從 Java 編寫的程式對關聯式資料庫的程式存取。它還表明 JDBC API 是 Java 平台的一部分,因此包含在 Java SE 和 Java EE 中。JDBC API 在兩個套件中提供:java.sql 和 javax.sql。那我們就來認識他們吧。
JDBC 或一切的開始 - 3

工作開始

要了解 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
正如我們所看到的,這是合乎邏輯的,資料庫是一個外部元件,不是 Java SE 原生的。解釋起來很簡單——資料庫數量巨大,您可以使用任何一個。例如,有PostgreSQL、Oracle、MySQL、H2。這些資料庫中的每一個都由稱為資料庫供應商的獨立公司提供。每個資料庫都是用自己的程式語言(不一定是 Java)編寫的。為了能夠從 Java 應用程式使用資料庫,資料庫提供者編寫了一個特殊的驅動程序,它是它自己的圖像適配器。這種相容於 JDBC 的資料庫(即具有 JDBC 驅動程式的資料庫)也稱為「JDBC 相容資料庫」。這裡我們可以用電腦設備來類比。例如,在記事本中有一個「列印」按鈕。每次按下它時,程式都會告訴作業系統記事本應用程式想要列印。而且你有一台印表機。要教導您的作業系統與 Canon 或 HP 印表機統一通信,您將需要不同的驅動程式。但對於身為使用者的您來說,什麼都不會改變。您仍然會按下同一個按鈕。與 JDBC 相同。您正在運行相同的程式碼,只是不同的資料庫可能在背景運行。我認為這是一個非常明確的方法。每個這樣的 JDBC 驅動程式都是某種工件、函式庫、jar 檔案。這是我們專案的依賴項。例如,我們可以選擇資料庫“ H2 Database ”,然後我們需要新增以下依賴項:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
如何找到依賴項以及如何描述它在資料庫提供者的官方網站或「Maven Central」上都有說明。如您所知,JDBC 驅動程式不是資料庫。但他只是一個嚮導。但是有一種叫做「記憶體資料庫」的東西。這些資料庫在應用程式的整個生命週期內都存在於記憶體中。通常,這通常用於測試或培訓目的。這使您可以避免在電腦上安裝單獨的資料庫伺服器。這非常適合我們熟悉JDBC。我們的沙箱已準備就緒,我們可以開始了。
JDBC 或一切的開始 - 5

聯繫

所以,我們有一個 JDBC 驅動程序,我們有一個 JDBC API。我們記得,JDBC 代表 Java 資料庫連線。因此,這一切都始於連結性——建立連結的能力。連接就是連接。讓我們再次翻到JDBC 規範的正文並查看目錄。在「 CHAPTER 4 Overview 」(概述)一章中我們翻到「4.1Establishing a Connection」(建立連接)一節,據說有兩種方式連接資料庫:
  • 透過驅動管理器
  • 透過資料來源
讓我們來處理DriverManager。如前所述,DriverManager 允許您連接到指定 URL 處的資料庫,並且還載入它在 CLASSPATH 中找到的 JDBC 驅動程式(在 JDBC 4.0 之前,您必須自行載入驅動程式類別)。有一個單獨的章節「第 9 章連接」介紹連接到資料庫的內容。我們感興趣的是如何透過 DriverManager 取得連接,因此我們對「9.3 DriverManager 類別」部分感興趣。它指示我們如何存取資料庫:
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

聲明

接下來我們對語句或表達式感興趣。它們在「第 13 章聲明」一章的文檔中進行了描述。首先,它說有幾種類型或類型的語句:
  • 語句:不包含參數的SQL表達式
  • PreparedStatement :包含輸入參數的已準備好的 SQL 語句
  • CallableStatement:能夠從 SQL 預存程序取得傳回值的 SQL 表達式。
因此,有了連接,我們就可以在該連接的框架內執行一些請求。因此,我們最初從 Connection 取得 SQL 表達式的實例是合乎邏輯的。您需要從建立一個表開始。讓我們將表創建請求描述為字串變數。怎麼做?讓我們使用一些教學課程,例如「sqltutorial.org」、「sqlbolt.com」、「postgresqltutorial.com」、「codecademy.com」。例如,我們使用khanacademy.org上的 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

結果集

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

元數據

除了查詢之外,到資料庫的連線(即 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

連接池

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

交易

JDBC 最有趣的事情之一就是交易。在 JDBC 規範中,它們被指派到「第 10 章事務」一章。首先,有必要了解什麼是交易。事務是一組對資料進行邏輯組合的順序操作,作為一個整體進行處理或取消。使用 JDBC 時交易何時開始?如規範所述,這是由 JDBC 驅動程式直接處理的。但通常情況下,噹噹前 SQL 語句需要事務且事務尚未建立時,新事務就會開始。交易什麼時候結束?這是由自動提交屬性控制的。如果啟用自動提交,則交易將在 SQL 語句「完成」後完成。「完成」的意思取決於 SQL 表達式的類型:
  • 資料操作語言,也稱為DML(插入、更新、刪除)
    操作完成後事務就完成
  • Select 語句
    當 ResultSet 關閉時交易完成(ResultSet#close
  • 傳回多個結果的 CallableStatement 和表達式
    當所有關聯的 ResultSet 均已關閉且所有輸出均已收到(包括更新次數)時
這正是 JDBC API 的行為。像往常一樣,讓我們為此編寫一個測試:
@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

絕緣等級

讓我們開啟 JDBC 規範的「10.2 交易隔離等級」小節。在這裡,在進一步討論之前,我想記住 ACID 這樣的事情。ACID 描述了事務系統的需求。
  • 原子性:
    任何事務都不會部分提交給系統。要么執行其所有子操作,要么不執行任何子操作。
  • 一致性:
    根據定義,每筆成功的交易只記錄有效的結果。
  • 隔離性:
    交易運行時,並發事務不應影響其結果。
  • 持久性:
    如果交易成功完成,對其所做的變更不會因任何失敗而撤銷。
當談論事務隔離等級時,我們談論的是「隔離」要求。隔離是一項昂貴的要求,因此在實際資料庫中存在無法完全隔離交易的模式(可重複讀取隔離等級及更低層級)。維基百科對處理事務時可能出現的問題有很好的解釋。這裡值得閱讀更多內容:「使用事務進行並行存取的問題」。在編寫測試之前,讓我們稍微更改一下 Gradle 建置腳本:新增一個帶有屬性的區塊,也就是我們專案的設定:
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

底線

正如我們所看到的,JDBC 是 Java 手中處理資料庫的強大工具。我希望這篇簡短的評論能為您提供一個起點或幫助您刷新記憶。好吧,作為零食,需要一些額外的材料: #維亞切斯拉夫
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION