使用 HarperDB 和 Java
1. 概述
在本教程中,我們將討論 Java 對HarperDB的支持, HarperDB 是一個具有 SQL 功能的高效能且靈活的 NoSQL 資料庫。毫無疑問,標準 Java 資料庫連接有助於將其與各種領先的 BI、報告、ETL 工具和自訂應用程式整合。它還提供用於執行資料庫管理和操作的REST API 。
然而,JDBC 簡化並加速了 HarperDB 在應用程式中的採用。它可能會顯著簡化並加快該過程。
在本教程中,我們將使用 Java 測試容器庫。這將使我們能夠運行 HarperDB Docker 容器並展示即時整合。
讓我們透過一些範例來探討 HarperDB 可用的 JDBC 支援範圍。
2. JDBC庫
HarperDB 附帶了一個JDBC 函式庫,我們將其匯入到pom.xml
檔案中:
<dependency>
<groupId>com.baeldung</groupId>
<artifactId>java-harperdb</artifactId>
<version>4.2</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/cdata.jdbc.harperdb.jar</systemPath>
</dependency>
由於它在公共 Maven 儲存庫上不可用,因此我們必須從本機目錄或私人 Maven 儲存庫匯入它。
3. 建立 JDBC 連接
在開始在 Harper DB 中執行 SQL 語句之前,我們將探討如何取得java.sql.Connection
物件。
讓我們從第一個選項開始:
@Test
void whenConnectionInfoInURL_thenConnectSuccess() {
assertDoesNotThrow(() -> {
final String JDBC_URL = "jdbc:harperdb:Server=127.0.0.1:" + port + ";User=admin;Password=password;";
try (Connection connection = DriverManager.getConnection(JDBC_URL)) {
connection.createStatement().executeQuery("select 1");
logger.info("Connection Successful");
}
});
}
除了 JDBC URL 中的前綴jdbc:harperdb:
之外,與取得關聯式資料庫的連線相比沒有太大差異。通常,密碼在傳遞到 URL 之前應始終進行加密和解密。
繼續,讓我們看看第二個選項:
@Test
void whenConnectionInfoInProperties_thenConnectSuccess() {
assertDoesNotThrow(() -> {
Properties prop = new Properties();
prop.setProperty("Server", "127.0.0.1:" + port);
prop.setProperty("User", "admin");
prop.setProperty("Password", "password");
try (Connection connection = DriverManager.getConnection("jdbc:harperdb:", prop)) {
connection.createStatement().executeQuery("select 1");
logger.info("Connection Successful");
}
});
}
與先前的選項相反,我們使用Properties
物件將連線詳細資訊傳遞給DriveManager
。
應用程式通常使用連接池來獲得最佳效能。因此,可以合理地預期 HarperDB 的 JDBC 驅動程式也包含相同的內容:
@Test
void whenConnectionPooling_thenConnectSuccess() {
assertDoesNotThrow(() -> {
HarperDBConnectionPoolDataSource harperdbPoolDataSource = new HarperDBConnectionPoolDataSource();
final String JDBC_URL = "jdbc:harperdb:UseConnectionPooling=true;PoolMaxSize=2;Server=127.0.0.1:" + port
+ ";User=admin;Password=password;";
harperdbPoolDataSource.setURL(JDBC_URL);
try(Connection connection = harperdbPoolDataSource.getPooledConnection().getConnection()) {
connection.createStatement().executeQuery("select 1");
logger.info("Connection Successful");
}
});
}
為了啟用連線池,我們使用了屬性UseConnectionPooling=true
。另外,我們必須使用驅動程式類別HarperDBConnectionPoolDataSource
來取得連線池。
此外,其他連線屬性可用於更多選項。
4. 建立架構和表
HaperDB提供RESTful資料庫操作API用於設定和管理資料庫。它還具有用於建立資料庫物件並對其執行 SQL CRUD 操作的 API。
但是,不支援Create Table, Create Schema,
等 DDL 語句。但是, **HarperDB 提供了用於建立模式和表格的預存程序**:
@Test
void whenExecuteStoredToCreateTable_thenSuccess() throws SQLException {
final String CREATE_TABLE_PROC = "CreateTable";
try (Connection connection = getConnection()) {
CallableStatement callableStatement = connection.prepareCall(CREATE_TABLE_PROC);
callableStatement.setString("SchemaName", "Prod");
callableStatement.setString("TableName", "Subject");
callableStatement.setString("PrimaryKey", "id");
Boolean result = callableStatement.execute();
ResultSet resultSet = callableStatement.getResultSet();
while (resultSet.next()) {
String tableCreated = resultSet.getString("Success");
assertEquals("true", tableCreated);
}
}
}
CallableStatement
執行CreateTable
預存程序並在Prod
模式中建立表Subject
。程序將SchemaName
、 TableName,
和PrimaryKey
作為輸入參數。有趣的是,我們沒有明確創建模式。如果資料庫中不存在該架構,則會建立該架構。
類似地,其他預存程序(如 CreateHarperSchema、DropSchema、DropTable 等)可以由CallableStatement
呼叫。
5.增刪改查支持
HarperDB JDBC 驅動程式支援 CRUD 操作。我們可以使用java.sql.Statement
和java.sql.PreparedSatement
從表格中建立、查詢、更新和刪除記錄。
5.1.資料庫模型
在繼續進行下一部分之前,我們先設定一些用於執行 SQL 語句的資料。我們假設一個名為Demo
資料庫模式有三個表:
Subject
和Teacher
是兩個主表。表Teacher_Details
包含教師所教授科目的詳細資料。出乎意料的是,在欄位teacher_id
和subject_id
上沒有外鍵約束,因為 HarperDB 不支援它。
我們來看看Subject
表中的資料:
[
{"id":1, "name":"English"},
{"id":2, "name":"Maths"},
{"id":3, "name":"Science"}
]
同樣,我們看一下Teacher
表中的資料:
[
{"id":1, "name":"James Cameron", "joining_date":"04-05-2000"},
{"id":2, "name":"Joe Biden", "joining_date":"20-10-2005"},
{"id":3, "name":"Jessie Williams", "joining_date":"04-06-1997"},
{"id":4, "name":"Robin Williams", "joining_date":"01-01-2020"},
{"id":5, "name":"Eric Johnson", "joining_date":"04-05-2022"},
{"id":6, "name":"Raghu Yadav", "joining_date":"02-02-1999"}
]
現在,讓我們來看看Teacher_Details
表中的記錄:
[
{"id":1, "teacher_id":1, "subject_id":1},
{"id":2, "teacher_id":1, "subject_id":2},
{"id":3, "teacher_id":2, "subject_id":3 },
{"id":4, "teacher_id":3, "subject_id":1},
{"id":5, "teacher_id":3, "subject_id":3},
{"id":6, "teacher_id":4, "subject_id":2},
{"id":7, "teacher_id":5, "subject_id":3},
{"id":8, "teacher_id":6, "subject_id":1},
{"id":9, "teacher_id":6, "subject_id":2},
{"id":15, "teacher_id":6, "subject_id":3}
]
值得注意的是,所有表中的列id
都是主鍵。
5.2.使用Insert
建立記錄
讓我們透過在Subject
表中建立一些記錄來引入更多主題:
@Test
void givenStatement_whenInsertRecord_thenSuccess() throws SQLException {
final String INSERT_SQL = "insert into Demo.Subject(id, name) values "
+ "(4, 'Social Studies'),"
+ "(5, 'Geography')";
try (Connection connection = getConnection()) {
Statement statement = connection.createStatement();
assertDoesNotThrow(() -> statement.execute(INSERT_SQL));
assertEquals(2, statement.getUpdateCount());
}
}
我們使用java.sql.Statement
將兩筆記錄插入到Subject
表中。
讓我們透過考慮Teacher
表,在java.sql.PrepareStatement
的幫助下實現一個更好的版本:
@Test
void givenPrepareStatement_whenAddToBatch_thenSuccess() throws SQLException {
final String INSERT_SQL = "insert into Demo.Teacher(id, name, joining_date) values"
+ "(?, ?, ?)";
try (Connection connection = getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(INSERT_SQL);
preparedStatement.setInt(1, 7);
preparedStatement.setString(2, "Bret Lee");
preparedStatement.setString(3, "07-08-2002");
preparedStatement.addBatch();
preparedStatement.setInt(1, 8);
preparedStatement.setString(2, "Sarah Glimmer");
preparedStatement.setString(3, "07-08-1997");
preparedStatement.addBatch();
int[] recordsInserted = preparedStatement.executeBatch();
assertEquals(2, Arrays.stream(recordsInserted).sum());
}
}
因此,我們參數化了insert
語句,並使用addBatch()
和executeBatch()
方法批次執行它們。批量執行對於處理大量記錄至關重要。因此,它對 HarperDB 的 JDBC 驅動程式的支援非常有價值。
5.3.使用Insert Into Select
建立記錄
HarperDB JDBC 驅動程式還提供了在執行時間建立臨時表的功能。此臨時表稍後可用於透過單一insert into select
語句插入最終目標表。與批次執行類似,這也有助於減少對資料庫的呼叫次數。
讓我們看看這個功能的實際效果:
@Test
void givenTempTable_whenInsertIntoSelectTempTable_thenSuccess() throws SQLException {
try (Connection connection = getConnection()) {
Statement statement = connection.createStatement();
assertDoesNotThrow(() -> {
statement.execute("insert into Teacher#TEMP(id, name, joining_date) "
+ "values('12', 'David Flinch', '04-04-2014')");
statement.execute("insert into Teacher#TEMP(id, name, joining_date) "
+ "values('13', 'Stephen Hawkins', '04-07-2017')");
statement.execute("insert into Teacher#TEMP(id, name, joining_date) "
+ "values('14', 'Albert Einstein', '12-08-2020')");
statement.execute("insert into Teacher#TEMP(id, name, joining_date) "
+ "values('15', 'Leo Tolstoy', '20-08-2022')");
});
assertDoesNotThrow(() -> statement.execute("insert into Demo.Teacher(id, name, joining_date) "
+ "select id, name, joining_date from Teacher#TEMP"));
ResultSet resultSet = statement.executeQuery("select count(id) as rows from Demo.Teacher where id in"
+ " (12, 13, 14, 15)");
resultSet.next();
int totalRows = resultSet.getInt("rows");
assertEquals(4, totalRows);
}
}
所有臨時表的格式應為[table name]#TEMP
,如Teacher#TEMP
中所示。一旦我們執行insert
語句,它就會被創建。臨時表Teacher#TEMP
中插入了四筆記錄。然後,透過單一insert into select
語句,所有記錄都被插入到目標Teacher
表中。
5.4.從表中讀取記錄
讓我們先在java.sql.Statement
的幫助下查詢Subject
表:
@Test
void givenStatement_whenFetchRecord_thenSuccess() throws SQLException {
final String SQL_QUERY = "select id, name from Demo.Subject where name = 'Maths'";
try (Connection connection = getConnection()) {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(SQL_QUERY);
while (resultSet.next()) {
Integer id = resultSet.getInt("id");
String name = resultSet.getString("name");
assertNotNull(id);
logger.info("Subject id:" + id + " Subject Name:" + name);
}
}
}
java.sql.Statement
的executeQuery()
方法成功執行並取得記錄。
讓我們看看驅動程式是否支援java.sql.PrepareStatement
。這次讓我們執行一個有連接條件的查詢,讓它變得更加令人興奮和複雜:
@Test
void givenPreparedStatement_whenExecuteJoinQuery_thenSuccess() throws SQLException {
final String JOIN_QUERY = "SELECT t.name as teacher_name, t.joining_date as joining_date, s.name as subject_name "
+ "from Demo.Teacher_Details AS td "
+ "INNER JOIN Demo.Teacher AS t ON t.id = td.teacher_id "
+ "INNER JOIN Demo.Subject AS s on s.id = td.subject_id "
+ "where t.name = ?";
try (Connection connection = getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(JOIN_QUERY);
preparedStatement.setString(1, "Eric Johnson");
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String teacherName = resultSet.getString("teacher_name");
String subjectName = resultSet.getString("subject_name");
String joiningDate = resultSet.getString("joining_date");
assertEquals("Eric Johnson", teacherName);
assertEquals("Maths", subjectName);
}
}
}
我們不僅執行了參數化查詢,還發現 HarperDB 可以對非結構化資料執行聯結查詢。
5.5.從使用者定義的檢視中讀取記錄
HarperDB 驅動程式具有建立使用者定義視圖的功能。這些是虛擬視圖,可用於我們無法存取表格查詢的場景,即從工具使用驅動程式時。
讓我們在檔案UserDefinedViews.json:
{
"View_Teacher_Details": {
"query": "SELECT t.name as teacher_name, t.joining_date as joining_date, s.name as subject_name from Demo.Teacher_Details AS td
INNER JOIN Demo.Teacher AS t ON t.id = td.teacher_id INNER JOIN Demo.Subject AS s on s.id = td.subject_id"
}
}
該查詢透過連接所有表格來獲取教師的詳細資訊。視圖的預設架構是UserViews
。
驅動程式在連線屬性 Location 定義的目錄中尋找UserDefinedViews.json
[Location .](https://cdn.cdata.com/help/FHF/jdbc/RSBHarperDB_p_Location.htm)
讓我們看看這是如何運作的:
@Test
void givenUserDefinedView_whenQueryView_thenSuccess() throws SQLException {
URL url = ClassLoader.getSystemClassLoader().getResource("UserDefinedViews.json");
String folderPath = url.getPath().substring(0, url.getPath().lastIndexOf('/'));
try(Connection connection = getConnection(Map.of("Location", folderPath))) {
PreparedStatement preparedStatement = connection.prepareStatement("select teacher_name,subject_name"
+ " from UserViews.View_Teacher_Details where subject_name = ?");
preparedStatement.setString(1, "Science");
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()) {
assertEquals("Science", resultSet.getString("subject_name"));
}
}
}
為了建立資料庫連接,程式將檔案UserDefinedViews.json
的資料夾路徑傳遞給方法getConnection()
。之後,驅動程式在視圖View_Teacher_Details
上執行查詢並取得所有教授Science
的教師的詳細資訊。
5.6.從快取中保存和讀取記錄
應用程式更喜歡快取經常使用和存取的資料以提高效能。 HaperDB 驅動程式允許在本機磁碟或資料庫等位置快取資料。
對於我們的範例,我們將使用嵌入式 Derby 資料庫作為 Java 應用程式中的快取。但也可以選擇其他資料庫進行快取。
讓我們對此進行更多探索:
@Test
void givenAutoCache_whenQuery_thenSuccess() throws SQLException {
URL url = ClassLoader.getSystemClassLoader().getResource("test.db");
String folderPath = url.getPath().substring(0, url.getPath().lastIndexOf('/'));
logger.info("Cache Location:" + folderPath);
try(Connection connection = getConnection(Map.of("AutoCache", "true", "CacheLocation", folderPath))) {
PreparedStatement preparedStatement = connection.prepareStatement("select id, name from Demo.Subject");
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()) {
logger.info("Subject Name:" + resultSet.getString("name"));
}
}
}
我們使用了兩個連線屬性AutoCache
和CacheLocation
。 AutoCache=true
表示對錶的所有查詢都會快取到屬性CacheLocation
中指定的位置。但是,驅動程式也使用CACHE statements
提供明確快取功能。
5.7.更新記錄
讓我們來看一個使用java.sql.Statement
更新老師教授的科目的範例:
@Test
void givenStatement_whenUpdateRecord_thenSuccess() throws SQLException {
final String UPDATE_SQL = "update Demo.Teacher_Details set subject_id = 2 "
+ "where teacher_id in (2, 5)";
final String UPDATE_SQL_WITH_SUB_QUERY = "update Demo.Teacher_Details "
+ "set subject_id = (select id from Demo.Subject where name = 'Maths') "
+ "where teacher_id in (select id from Demo.Teacher where name in ('Joe Biden', 'Eric Johnson'))";
try (Connection connection = getConnection()) {
Statement statement = connection.createStatement();
assertDoesNotThrow(() -> statement.execute(UPDATE_SQL));
assertEquals(2, statement.getUpdateCount());
}
try (Connection connection = getConnection()) {
assertThrows(SQLException.class, () -> connection.createStatement().execute(UPDATE_SQL_WITH_SUB_QUERY));
}
}
當我們直接使用教師和科目的id
並且不從其他表中查找值時,第一個update
語句會成功執行。但是,當我們嘗試從Teacher
表和Subject
中找到id
值時,第二次更新失敗。發生這種情況是因為目前 HarperDB 不支援子查詢。
讓我們使用java.sql.PreparedStatement
來更新老師教授的科目:
@Test
void givenPreparedStatement_whenUpdateRecord_thenSuccess() throws SQLException {
final String UPDATE_SQL = "update Demo.Teacher_Details set subject_id = ? "
+ "where teacher_id in (?, ?)";
try (Connection connection = getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(UPDATE_SQL);
preparedStatement.setInt(1, 1);
//following is not supported by the HarperDB driver
//Integer[] teacherIds = {4, 5};
//Array teacherIdArray = connection.createArrayOf(Integer.class.getTypeName(), teacherIds);
preparedStatement.setInt(2, 4);
preparedStatement.setInt(3, 5);
assertDoesNotThrow(() -> preparedStatement.execute());
assertEquals(2, preparedStatement.getUpdateCount());
}
}
不幸的是,HarperDB JDBC 驅動程式不支援建立java.sql.Array
對象,因此我們無法將教師id
陣列作為in
子句中的參數傳遞。這就是為什麼我們必須多次呼叫setInt()
來設定教師id
。這是一個缺點,可能會帶來很多不便。
5.8.刪除記錄
讓我們對Teacher_Details
表執行delete
語句:
@Test
void givenStatement_whenDeleteRecord_thenSuccess() throws SQLException {
final String DELETE_SQL = "delete from Demo.Teacher_Details where teacher_id = 6 and subject_id = 3";
try (Connection connection = getConnection()) {
Statement statement = connection.createStatement();
assertDoesNotThrow(() -> statement.execute(DELETE_SQL));
assertEquals(1, statement.getUpdateCount());
}
}
java.sql.Statement
幫助成功刪除了記錄。
繼續,讓我們嘗試使用java.sql.PreparedStatement
:
@Test
void givenPreparedStatement_whenDeleteRecord_thenSuccess() throws SQLException {
final String DELETE_SQL = "delete from Demo.Teacher_Details where teacher_id = ? and subject_id = ?";
try (Connection connection = getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(DELETE_SQL);
preparedStatement.setInt(1, 6);
preparedStatement.setInt(2, 2);
assertDoesNotThrow(() -> preparedStatement.execute());
assertEquals(1, preparedStatement.getUpdateCount());
}
}
我們可以參數化並成功執行delete
語句。
六,結論
在本文中,我們了解了 HarperDB 中的 JDBC 支援。 HarperDB 是一個 NoSQL 資料庫,但它的 JDBC 驅動程式使 Java 應用程式能夠執行 SQL 語句。 HarperDB 尚未支援一些 SQL 功能。
此外,該驅動程式也不是百分百符合 JDBC 協定。但它用一些功能來彌補它,例如使用者定義的視圖、臨時表、快取等。
與往常一樣,本文範例中使用的程式碼可以在 GitHub 上找到。