DuckDB 作為一款高效能的記憶體資料函式庫,在 Java 應用中能透過 JDBC 驅動程式輕鬆整合。透過標準 JDBC API,開發者可以建立資料函式庫連線、執行 SQL 查詢,並處理結果集。DuckDB 也支援多執行緒存取,允許多個執行緒同時操作資料函式庫,提升應用程式效能。此外,DuckDB 能直接處理 Parquet 等格式的檔案,簡化 Java 應用程式處理這類別資料的流程,無需引入額外函式庫,降低專案依賴複雜度。以下程式碼片段展示瞭如何使用 DuckDBAppender 高效插入大量資料,透過 beginRow()、append() 和 endRow() 等方法,簡化資料寫入流程,提升效能。
使用Java透過JDBC驅動程式操作DuckDB
DuckDB是一種OLAP(線上分析處理)資料函式庫,能夠支援多執行緒存取。在Java應用程式中,可以使用DuckDB的JDBC驅動程式來與DuckDB資料函式庫進行互動。本章節將介紹如何使用DuckDB的JDBC驅動程式來操作DuckDB資料函式庫。
瞭解一般使用模式
使用DuckDB JDBC驅動程式的一般模式與其他JDBC驅動程式相同。對於一個純粹的記憶體資料函式庫,可以使用jdbc:duckdb:作為JDBC URL。以下是一個範例程式碼,示範如何取得連線、建立JDBC陳述式、執行查詢並列印結果:
import java.sql.DriverManager;
import java.sql.SQLException;
class simple {
public static void main(String... a) throws SQLException {
var query = "SELECT * FROM duckdb_settings() ORDER BY name";
try (
var con = DriverManager.getConnection("jdbc:duckdb:");
var stmt = con.createStatement();
var resultSet = stmt.executeQuery(query)
) {
while (resultSet.next()) {
System.out.printf("%s %s%n",
resultSet.getString("name"),
resultSet.getString("value"));
}
}
}
}
內容解密:
DriverManager.getConnection("jdbc:duckdb:"):取得與DuckDB資料函式庫的連線。con.createStatement():建立一個JDBC陳述式,用於執行SQL查詢。stmt.executeQuery(query):執行SQL查詢並傳回結果集。resultSet.next():迭代結果集中的每一行。resultSet.getString("name")和resultSet.getString("value"):取得每一行中的欄位值。
在多執行緒中使用多個連線
在Java應用程式中,可以使用多個執行緒來存取DuckDB資料函式庫。以下是一個範例程式碼,示範如何在多個執行緒中使用多個連線來插入資料:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.duckdb.DuckDBConnection;
class using_multiple_connections {
private static final AtomicInteger ID_GENERATOR = new AtomicInteger(0);
private static final String DUCKDB_URL = "jdbc:duckdb:readings.db";
public static void main(String... a) throws Exception {
var createTableStatement = """
CREATE TABLE IF NOT EXISTS readings (
id INTEGER NOT NULL PRIMARY KEY,
created_on TIMESTAMP NOT NULL,
power DECIMAL(10,3) NOT NULL
)
""";
var executor = Executors.newWorkStealingPool();
try (
var con = DriverManager.getConnection(DUCKDB_URL);
var stmt = con.createStatement()
) {
stmt.execute(createTableStatement);
var result = stmt.executeQuery("SELECT max(id) + 1 FROM readings");
result.next();
ID_GENERATOR.compareAndSet(0, result.getInt(1));
result.close();
for (int i = 0; i < 20; ++i) {
executor.submit(() -> insertNewReading(con));
}
executor.shutdown();
executor.awaitTermination(5, TimeUnit.MINUTES);
}
}
static void insertNewReading(Connection connection) {
var sql = "INSERT INTO readings VALUES (?, ?, ?)";
var readOn = Timestamp.valueOf(LocalDateTime.now());
var value = ThreadLocalRandom.current().nextDouble() * 100;
try (
var duckCon = connection.unwrap(DuckDBConnection.class);
var duplicatedCon = duckCon.duplicate();
var stmt = duplicatedCon.prepareStatement(sql)
) {
stmt.setInt(1, ID_GENERATOR.getAndIncrement());
stmt.setTimestamp(2, readOn);
stmt.setDouble(3, value);
stmt.execute();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
內容解密:
DriverManager.getConnection(DUCKDB_URL):取得與DuckDB資料函式庫的連線。con.createStatement():建立一個JDBC陳述式,用於執行SQL查詢。stmt.execute(createTableStatement):建立一個名為readings的表格。Executors.newWorkStealingPool():建立一個執行緒池,用於執行多個任務。executor.submit(() -> insertNewReading(con)):提交一個任務,用於插入新的讀取資料。insertNewReading(Connection connection):插入新的讀取資料到readings表格中。connection.unwrap(DuckDBConnection.class):將連線轉換為DuckDBConnection物件。duckCon.duplicate():複製一個新的連線,用於在多個執行緒中使用。stmt.setInt(1, ID_GENERATOR.getAndIncrement())、stmt.setTimestamp(2, readOn)和stmt.setDouble(3, value):設定SQL陳述式中的引數值。stmt.execute():執行SQL陳述式,將新的讀取資料插入到readings表格中。
使用DuckDB JDBC驅動程式從Java存取資料
A.5.3 將DuckDB作為Java資料處理工具
在第5章中,我們使用DuckDB來探索資料,而無需使用DuckDB的持久儲存。當然,這不僅可以從DuckDB CLI或Python客戶端實作,也可以從Java實作。對於某些格式,如Parquet,這是一個很好的解決方案:Java無法在不使用外部函式庫的情況下處理Parquet檔案。Java中處理Parquet的函式庫通常依賴於Apache Hadoop、Apache Spark或Apache Avro——這些都是很好的產品,但它們帶有很多依賴項。如果您希望專案的依賴項較少,可以簡單地使用DuckDB。
我們的範例儲存函式庫的a1/weather資料夾中有一個Parquet檔案列表,其中包含從Wikipedia抓取的天氣資料。我們希望在Java程式中按名稱和年平均溫度值列出這些天氣站。我們可以要求嵌入式DuckDB例項為我們完成這項工作,而不是編寫大量Java程式碼來逐一載入和檢查檔案。下面列出的方法開啟一個記憶體連線,選擇感興趣的資料,並建立我們的列表。該方法本身是using_the_appender.java檔案的一部分。稍後將顯示整個程式。
清單A.5 using_the_appender::weatherStations
private record WeatherStation(String id, double avgTemperature) {
}
static List<WeatherStation> weatherStations() throws SQLException {
var query = """
SELECT City AS id,
cast(replace(
trim(
regexp_extract(Year,'(.*)\\n.*', 1)
), '−', '-') AS double)
AS avgTemperature
FROM 'weather/*.parquet'
""";
var weatherStations = new ArrayList<WeatherStation>();
try (
var con = DriverManager.getConnection("jdbc:duckdb:");
var stmt = con.createStatement();
var resultSet = stmt.executeQuery(query)
) {
while (resultSet.next()) {
var id = resultSet.getString("id");
var avgTemperature = resultSet.getDouble("avgTemperature");
weatherStations.add(new WeatherStation(id, avgTemperature));
}
}
return weatherStations;
}
內容解密:
private record WeatherStation(String id, double avgTemperature):定義了一個名為WeatherStation的記錄類別,用於儲存天氣站的ID和平均溫度。weatherStations()方法:該方法負責從Parquet檔案中提取天氣站的資料。query變數:定義了一個SQL查詢,使用DuckDB的Glob功能讀取weather資料夾中的所有Parquet檔案,並提取所需的資料。try-with-resources陳述式:確保了資料函式庫連線、陳述式和結果集在使用後正確關閉。while (resultSet.next()):遍歷查詢結果,將每行資料轉換為WeatherStation物件並加入列表。
A.5.4 插入大量資料
通常,您會使用java.sql.PreparedStatement例項及其批次處理功能來高效地插入大量資料。下面展示瞭如何使用org.duckdb.DuckDBAppender直接將資料寫入表格。
使用DuckDBAppender插入資料
try (
var con = DriverManager.getConnection("jdbc:duckdb:weather.db");
var duckCon = con.unwrap(DuckDBConnection.class);
var appender = duckCon.createAppender("weather")
) {
// 假設我們有一些要插入的資料
for (var station : weatherStations()) {
appender.beginRow();
appender.append(station.id());
appender.append(station.avgTemperature());
appender.endRow();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
內容解密:
DriverManager.getConnection("jdbc:duckdb:weather.db"):建立到名為weather.db的DuckDB資料函式庫的連線。con.unwrap(DuckDBConnection.class):將JDBC連線解封裝為DuckDB連線,以便使用DuckDB特有的功能。duckCon.createAppender("weather"):為名為weather的表格建立一個Appender,用於插入資料。appender.beginRow()和appender.endRow():標記一個插入行的開始和結束。appender.append():將資料附加到當前行。
透過這種方式,您可以高效地將大量資料插入到DuckDB表格中,而無需先將資料寫入檔案。