Truy vấn đối tượng bộ nhớ và CSV thông qua SQL – Điều này thật sự khả thi!

01 Giới thiệu

Khi nói đến đa nguồn dữ liệu, bạn có thể nghĩ ngay đến giải pháp đa nguồn dữ liệu trong Mybatis-Plus. Tuy nhiên, cách tiếp cận hôm nay sẽ làm bạn bất ngờ hoàn toàn — ít nhất là khiến tôi cảm thấy ngạc nhiên.

Bạn có từng tưởng tượng được việc truy vấn các đối tượng trong bộ nhớ hay tệp CSV bằng ngôn ngữ SQL? Hay thậm chí là liên kết các bảng từ nhiều nguồn khác nhau? Trước đây, điều đó dường như không thể thực hiện được. Nhưng giờ đây, với Apache Calcite, mọi điều đều có thể.

02 Tổng quan

Apache Calcite là một khung nền tảng mạnh mẽ dành cho cơ sở dữ liệu, đặc biệt phù hợp để truy cập và tối ưu hóa dữ liệu từ nhiều nguồn dữ liệu khác nhau thông qua giao diện SQL tiêu chuẩn.

Calcite sử dụng các trình thích ứng (Adapters) để kết nối với các nguồn dữ liệu bên ngoài. Mỗi trình thích ứng giúp một loại dữ liệu bên ngoài như tệp CSV, MySQL, đối tượng trong bộ nhớ Java, Redis… được xem như một bảng có thể truy vấn bằng SQL trong hệ thống Calcite.

Calcite hoạt động như một "khung tối ưu hóa truy vấn", nằm ở tầng trên cùng của các nguồn dữ liệu, cung cấp một giao diện SQL thống nhất. Nó không lưu trữ dữ liệu mà chỉ tối ưu hóa kế hoạch truy vấn và đẩy xuống các nguồn dữ liệu gốc để thực thi. Calcite không phải là một cơ sở dữ liệu, không hỗ trợ các lệnh DML như INSERT, UPDATE hay DELETE.

Trang chủ: https://calcite.apache.org/

Các trình thích ứng được hỗ trợ:

03 Thông số chính

Chúng ta sẽ tìm hiểu từng bước từ đối tượng trong bộ nhớ, tệp CSV, và cơ sở dữ liệu MySQL, rồi cuối cùng là hiệu quả khi kết hợp truy vấn.

3.1 Các thư viện cần thiết

<!-- Apache Calcite -->
<dependency>
    <groupId>org.apache.calcite</groupId>
    <artifactId>calcite-core</artifactId>
    <version>1.38.0</version>
</dependency>
<dependency>
    <groupId>org.apache.calcite</groupId>
    <artifactId>calcite-csv</artifactId>
    <version>1.38.0</version>
</dependency>

3.2 Giải thích thông số

Tập tin cấu hình chính là một file JSON chứa các tham số sau:

{
  "version": "1.0",
  "defaultSchema": "tên schema mặc định",
  "schemas": [
    {
      "name": "tên schema",
      "type": "custom",
      "factory": "",
      "operand": {
      }
    }
  ]
}

Tham số cấp cao nhất

  • version: Phiên bản định nghĩa
  • defaultSchema: Schema mặc định
  • schemas: Danh sách các schema, kiểu mảng

Thông số cụ thể của mỗi schema

  • name: Tên của schema
  • type: Kiểu schema. Có thể là: custom, map, jdbc
  • custom: Mở rộng, cần cấu hình factory
  • map: Dạng đơn giản dựa trên bộ nhớ, không cần factory
  • jdbc: Kết nối JDBC truyền thống
  • factory: Nhà máy tạo adapter
  • operand: Thông tin chi tiết về nguồn dữ liệu, mỗi loại sẽ có cấu hình khác nhau

04 Truy vấn đối tượng trong bộ nhớ

Truy vấn đối tượng trong bộ nhớ được hỗ trợ bởi calcite-core. Lớp schema chính là:

org.apache.calcite.adapter.java.ReflectiveSchema$Factory

Lưu ý ký tự $ biểu thị lớp nội bộ.

Cấu hình model

{
  "version": "1.0",
  "defaultSchema": "MEM",
  "schemas": [
    {
      "name": "MEM",
      "type": "custom",
      "factory": "org.apache.calcite.adapter.java.ReflectiveSchema$Factory",
      "operand": {
        "class": "com.simonking.boot.calcite.schema.UserSchema"
      }
    }
  ]
}

Đối với đối tượng trong bộ nhớ, Calcite dùng phản chiếu (reflection), vì vậy cần khai báo class tương ứng với nguồn dữ liệu.

4.1 Định nghĩa nguồn dữ liệu

public class UserSchema {

    public final User[] users;

    public UserSchema() {
        // Dữ liệu giả lập
        this.users = new User[]{
                new User(1, "zhangsan", 18),
                new User(2, "admin", 20),
                new User(3, "lisi", 22)
        };
    }

    /**
     * @Description: Định nghĩa thuộc tính
     **/
    @AllArgsConstructor
    public class User {

        /** ID người dùng */
        public final Integer id;
        /** Tên người dùng */
        public final String name;
        /** Tuổi */
        public final Integer age;
    }
}

Vì dữ liệu không thể chỉnh sửa, nên có thể khai báo lớp nguồn dữ liệu là final.

Các trường của bảng phải là trường công khai (public) và kiểu dữ liệu là mảng, nếu không sẽ không lấy được dữ liệu. Các trường của bảng phải thuộc lớp nội bộ, và các trường trong lớp nội bộ phải là công khai.

4.2 Truy vấn từ client

Giao thức truy vấn gần giống với JDBC thông thường.

@Test
void test01() throws Exception {
    CalciteConnection calciteConn = getCalciteConnection();

    String sql = "SELECT * FROM MEM.users";
    Statement stmt = calciteConn.createStatement();
    ResultSet rs = stmt.executeQuery(sql);
    doResult(rs);
}

Lưu ý rằng câu truy vấn phải bao gồm tên schema, nếu không sẽ không nhận diện đúng.

Phương thức lấy kết nối

 private static CalciteConnection getCalciteConnection() throws Exception {
    Class.forName("org.apache.calcite.jdbc.Driver");
    Properties prop = new Properties();
    prop.put("lex", "MYSQL");
    prop.put("model", "src/main/resources/calcite-model.json");
    Connection connection = DriverManager.getConnection("jdbc:calcite:", prop);
    CalciteConnection calciteConn = connection.unwrap(CalciteConnection.class);
    return calciteConn;
}

Các thông số trong Properties:

lex: Ngôn ngữ xử lý cú pháp, tương tự như kiểu SQL, mặc định là ORACLE. Đặt thành MYSQL để tên bảng không phân biệt hoa/thường.

model: Đường dẫn tới file cấu hình JSON.

Xuất kết quả

private void doResult(ResultSet rs) throws SQLException {
    ResultSetMetaData meta = rs.getMetaData();
    StringBuffer sb = new StringBuffer("[");
    while (rs.next()) {
        sb.append("{");
        for (int i = 1; i <= meta.getColumnCount(); i++) {
            sb.append(meta.getColumnName(i))
                .append(":")
                .append(rs.getObject(i))
                .append(",");
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append("},");
    }
    sb.deleteCharAt(sb.length() - 1);
    sb.append("]");
    System.out.println(sb);
}

Đây là phương pháp chuyển đổi kết quả truy vấn sang định dạng JSON để dễ đọc.

4.3 Kết quả truy vấn

05 Truy vấn tệp CSV

Để truy vấn tệp CSV, cần thêm thư viện mở rộng calcite-csv. Lớp schema chính:

org.apache.calcite.adapter.csv.CsvSchemaFactory

Cấu hình model

Chỉ cần bổ sung thêm schema mới.

{
  "version": "1.0",
  "defaultSchema": "MEM",
  "schemas": [
    {
      "name": "MEM",
      "type": "custom",
      "factory": "org.apache.calcite.adapter.java.ReflectiveSchema$Factory",
      "operand": {
        "class": "com.simonking.boot.calcite.schema.UserSchema"
      }
    },
    {
      "name": "CSV",
      "type": "custom",
      "factory": "org.apache.calcite.adapter.csv.CsvSchemaFactory",
      "operand": {
        "directory": "csv",
        "flavor": "scannable"
      }
    }
  ]
}

Thông số của nguồn dữ liệu: directory chỉ vị trí thư mục chứa CSV, mặc định nằm trong classpath. flavor xác định kiểu truy vấn, mặc định là scannable.

4.1 Định nghĩa tệp CSV

Chỉ cần tạo tệp CSV, tên tệp sẽ trở thành tên bảng.

4.2 Truy vấn từ client

@Test
void test02() throws Exception {
    CalciteConnection calciteConn = getCalciteConnection();

    String sql = "SELECT * FROM CSV.orders";
    Statement stmt = calciteConn.createStatement();
    ResultSet rs = stmt.executeQuery(sql);
    doResult(rs);
}

4.3 Kết quả truy vấn

06 Truy vấn MySQL

Truy vấn MySQL được hỗ trợ bởi calcite-core, lớp schema chính:

org.apache.calcite.adapter.jdbc.JdbcSchema$Factory

Cấu hình model

Chỉ cần thêm schema mới.

{
  "version": "1.0",
  "defaultSchema": "MEM",
  "schemas": [
    {
      "name": "MEM",
      "type": "custom",
      "factory": "org.apache.calcite.adapter.java.ReflectiveSchema$Factory",
      "operand": {
        "class": "com.simonking.boot.calcite.schema.UserSchema"
      }
    },
    {
      "name": "CSV",
      "type": "custom",
      "factory": "org.apache.calcite.adapter.csv.CsvSchemaFactory",
      "operand": {
        "directory": "csv",
        "flavor": "scannable"
      }
    },
    {
      "name": "MYSQL_DB",
      "type": "custom",
      "factory": "org.apache.calcite.adapter.jdbc.JdbcSchema$Factory",
      "operand": {
        "jdbcUrl": "jdbc:mysql://localhost:3306/test",
        "jdbcUser": "root",
        "jdbcPassword": "root"
      }
    }
  ]
}

6.1 Truy vấn từ client

Viết câu truy vấn như bình thường.

@Test
void test03() throws Exception {
    CalciteConnection calciteConn = getCalciteConnection();

    String sql = "SELECT * FROM MYSQL_DB.user_roles";
    Statement stmt = calciteConn.createStatement();
    ResultSet rs = stmt.executeQuery(sql);
    doResult(rs);
}

6.2 Kết quả truy vấn

07 Truy vấn liên kết

Dùng lại cấu hình trước đó, viết trực tiếp câu SQL liên kết.

08 Kết luận

Nhiều tổ chức chia cơ sở dữ liệu thành nhiều phần nhưng để giải quyết vấn đề truy vấn xuyên bộ, họ thường đồng bộ một cơ sở dữ liệu toàn cục. Với Apache Calcite, điều này hoàn toàn không còn cần thiết.

Trong quá trình thử nghiệm, tôi phát hiện rằng từ phiên bản 1.39.0 trở lên, khi truy vấn liên kết giữa các tệp CSV, có thể gặp lỗi trả về rỗng, dù truy vấn riêng lẻ vẫn hoạt động tốt. Có vẻ như đây là lỗi hoặc cải tiến mới của hệ thống. Ai biết rõ hơn vui lòng chia sẻ nhé.

Thẻ: Apache Calcite sql Java Memory Objects CSV Multi-source Data

Đăng vào ngày 15 tháng 6 lúc 22:05