在 Java 中多次重複使用 PreparedStatement
1. 概述
在本教程中,我們將研究如何有效地使用PreparedStatement
。 PreparedStatement
是一個為我們儲存預先編譯的 SQL 語句的物件。然後我們可以使用該物件重複執行該 SQL。在本文中,我們將看到有幾種方法可以做到這一點,選擇正確的方法可以顯著改善我們的程式碼和應用程式的效能。
2. 設定
首先,我們需要建立一個資料庫並取得可以使用的Connection
。讓我們建立一個名為CUSTOMER
的表,其中包含三個欄位; id
、 first_name,
和last_name
:
Connection connection = null;
void setupDatabaseAndConnect() throws SQLException {
connection = DriverManager.getConnection("jdbc:h2:mem:testDB", "dbUser", "dbPassword");
String createTable = "CREATE TABLE CUSTOMER (id INT, first_name TEXT, last_name TEXT)";
connection.createStatement().execute(createTable);
}
這裡我們連接到記憶體資料庫中的 H2,並將我們的Connection
物件儲存為類別級變量,以便我們以後可以在我們的方法中使用它。接下來,我們按照計劃使用該Connection
建立了表。我們在此處連接的資料庫類型和表格的定義並不重要,這只是為了讓我們以後的範例可以使用一些內容。
3. 準備語句的使用效率低下
這是我們第一次嘗試將一些資料放入資料庫,讓我們設定一個PreparedStatement
的基本用法,雖然它可以運作但遠非理想:
String SQL = "INSERT INTO CUSTOMER (id, first_name, last_name) VALUES(?,?,?)";
void inefficientUsage() throws SQLException {
for (int i = 0; i < 10000; i++) {
PreparedStatement preparedStatement = connection.prepareStatement(SQL);
preparedStatement.setInt(1, i);
preparedStatement.setString(2, "firstname" + i);
preparedStatement.setString(3, "secondname" + i);
preparedStatement.executeUpdate();
preparedStatement.close();
}
}
在這裡我們定義了 SQL String
,然後直接進入for
迴圈。對於循環的每次循環,我們建立一個PreparedStatement
,設定參數,執行更新,然後關閉它。
為了查看這是否有效,我們可以計算一下Customer
表中的列數。我們可以用另一個PreparedStatement
來實現這一點:
int checkRowCount() {
try (PreparedStatement counter = connection.prepareStatement("SELECT COUNT(*) AS customers FROM CUSTOMER")) {
ResultSet resultSet = counter.executeQuery();
resultSet.next();
int count = resultSet.getInt("customers");
resultSet.close();
return count;
} catch (SQLException e) {
return -1;
}
}
最後,讓我們在測試中調用它們並看看會發生什麼:
@Test
void whenCallingInefficientPreparedStatementMethod_thenRowsAreCreatedAsExpected() throws SQLException {
ReusePreparedStatement service = new ReusePreparedStatement();
service.setupDatabaseAndConnect();
service.inefficientUsage();
int rowsCreated = service.checkRowCount();
assertEquals(10000, rowsCreated);
}
我們可以看到一切都按預期進行,我們創建了 10,000 行。然而,這一結果遠非最佳。我們建立並關閉了PreparedStatement
物件 10,000 次。這對其他應用程式的效能的影響取決於我們執行此操作的頻率和循環的大小。然而,最好完全避免這種情況,因為有更好的方法,我們接下來將討論。
4. 簡單重用準備好的語句
按照我們的基本實現,明顯的改進是將PreparedStatement
創建移出for
循環。我們可以創建一次並根據需要多次使用它。我們可以做的另一個小改進是使用try-with-resources
來管理PreparedStatement
的生命週期。
讓我們用與上次相同的 SQL 來看看結果:
void betterUsage() {
try (PreparedStatement preparedStatement = connection.prepareStatement(SQL)) {
for (int i = 0; i < 10000; i++) {
preparedStatement.setInt(1, i);
preparedStatement.setString(2, "firstname" + i);
preparedStatement.setString(3, "secondname" + i);
preparedStatement.executeUpdate();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
所以現在我們只創建一次PreparedStatement
。我們也不必擔心呼叫close()
,因為我們已經處理好了。在實際實作中,我們當然希望更好地處理異常。這是我們程式碼中應該使用的最低效率。
讓我們編寫一個測試來檢查它是否按預期工作:
@Test
void whenCallingBetterPreparedStatementMethod_thenRowsAreCreatedAsExpected() throws SQLException {
ReusePreparedStatement servicehow to use PreparedStatements efficiently = new ReusePreparedStatement();
service.setupDatabaseAndConnect();
service.betterUsage();
int rowsCreated = service.checkRowCount();
assertEquals(10000, rowsCreated);
}
這看起來和之前的很熟悉。這是因為我們正在做同樣的事情。從功能上看,到目前為止我們的實作基本上相同。我們可以在這裡再次看到我們獲得了預期的 10,000 行。
這種方法還有更多潛在問題。首先,我們每次都會將更新發送到資料庫,因此需要進行大量的資料庫互動。此外,如果我們因任何原因被打斷,就很難繼續執行任務,更不用說在正確的地方了。如果不查看資料庫並查看我們計劃進行的每個更新,我們就無法知道我們到了哪裡。我們將在下一節中解決這個問題。
5. 透過批次提高效率
最後,讓我們找到重複使用PreparedStatement
的最佳選擇。這裡的關鍵是使用批次。
我們會將所有更新新增到一個批次中,並在準備好時最後執行該批次。透過這樣做,我們可以消除在任務進行到一半時發生故障而不知道我們到了哪裡的風險:
void bestUsage() {
try (PreparedStatement preparedStatement = connection.prepareStatement(SQL)) {
connection.setAutoCommit(false);
for (int i = 0; i < 10000; i++) {
preparedStatement.setInt(1, i);
preparedStatement.setString(2, "firstname" + i);
preparedStatement.setString(3, "secondname" + i);
preparedStatement.addBatch();
}
preparedStatement.executeBatch();
try {
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw e;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
這裡我們又在try-with-resources
中建立了一個PreparedStatement
。這次的不同之處在於,在開始循環之前,我們呼叫了setAutoCommit(false)
。這告訴Connection
將我們的 SQL 語句分組為事務,我們可以決定何時提交。然後在我們的for
循環中,我們將參數新增到批次中。只有當我們將它們全部添加之後,我們才會執行批次處理,並且假設一切順利,我們就會提交更改。如果提交期間出現任何問題,我們會捕獲異常並回滾到開始的地方。這意味著我們不會面臨未完成的工作和未知的資料庫狀態。
如果我們願意,我們可以更頻繁地執行和提交批次,例如每 5000 筆記錄。這意味著如果發生中斷,我們不會失去所有進展。如果我們這樣做了,我們可能會想要記錄下我們更新的進度。這將幫助我們在解決任何問題後恢復我們正在做的事情。
6. 結論
在本文中,我們研究了使用PreparedStatement
將資料插入資料庫的三種方法。我們首先為所做的每個更新創建一個,然後發現它運作良好但效率低。我們進展到在循環內重複使用一個,這樣更好,意味著我們只需要建立和關閉一次物件。最後,我們不僅在循環內重複使用同一個,而且還批量插入並定期執行它們。
因此,為了充分利用PreparedStatement,
我們需要創建一次,根據需要多次重複使用它,並且如果有很多更新則分批執行。
與往常一樣,範例的完整程式碼可在 GitHub 上找到。