返回文章列表

Java JDBC 驅動程式操作 DuckDB 資料函式庫

本文介紹如何使用 Java JDBC 驅動程式操作 DuckDB 資料函式庫,包含建立連線、執行查詢、多執行緒資料函式庫操作,以及利用 DuckDB 處理 Parquet 檔案等實務案例。文章提供程式碼範例,並詳細說明 DuckDB JDBC API 的使用方法,以及如何在 Java 應用中有效地與 DuckDB

資料函式庫 Java

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"));
            }
        }
    }
}

內容解密:

  1. DriverManager.getConnection("jdbc:duckdb:"):取得與DuckDB資料函式庫的連線。
  2. con.createStatement():建立一個JDBC陳述式,用於執行SQL查詢。
  3. stmt.executeQuery(query):執行SQL查詢並傳回結果集。
  4. resultSet.next():迭代結果集中的每一行。
  5. 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);
        }
    }
}

內容解密:

  1. DriverManager.getConnection(DUCKDB_URL):取得與DuckDB資料函式庫的連線。
  2. con.createStatement():建立一個JDBC陳述式,用於執行SQL查詢。
  3. stmt.execute(createTableStatement):建立一個名為readings的表格。
  4. Executors.newWorkStealingPool():建立一個執行緒池,用於執行多個任務。
  5. executor.submit(() -> insertNewReading(con)):提交一個任務,用於插入新的讀取資料。
  6. insertNewReading(Connection connection):插入新的讀取資料到readings表格中。
  7. connection.unwrap(DuckDBConnection.class):將連線轉換為DuckDBConnection物件。
  8. duckCon.duplicate():複製一個新的連線,用於在多個執行緒中使用。
  9. stmt.setInt(1, ID_GENERATOR.getAndIncrement())stmt.setTimestamp(2, readOn)stmt.setDouble(3, value):設定SQL陳述式中的引數值。
  10. 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;
}

內容解密:

  1. private record WeatherStation(String id, double avgTemperature):定義了一個名為WeatherStation的記錄類別,用於儲存天氣站的ID和平均溫度。
  2. 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);
}

內容解密:

  1. DriverManager.getConnection("jdbc:duckdb:weather.db"):建立到名為weather.db的DuckDB資料函式庫的連線。
  2. con.unwrap(DuckDBConnection.class):將JDBC連線解封裝為DuckDB連線,以便使用DuckDB特有的功能。
  3. duckCon.createAppender("weather"):為名為weather的表格建立一個Appender,用於插入資料。
  4. appender.beginRow()appender.endRow():標記一個插入行的開始和結束。
  5. appender.append():將資料附加到當前行。

透過這種方式,您可以高效地將大量資料插入到DuckDB表格中,而無需先將資料寫入檔案。