簡體   English   中英

使用JDBC將CSV復制到具有自定義類型數組的Postgres

[英]CSV copy to Postgres with array of custom type using JDBC

我在我的數據庫中定義了一個自定義類型

CREATE TYPE address AS (ip inet, port int);

以及在數組中使用此類型的表:

CREATE TABLE my_table (
  addresses  address[] NULL
)

我有一個包含以下內容的示例CSV文件

{(10.10.10.1,80),(10.10.10.2,443)}
{(10.10.10.3,8080),(10.10.10.4,4040)}

我使用以下代碼片段來執行我的COPY:

    Class.forName("org.postgresql.Driver");

    String input = loadCsvFromFile();

    Reader reader = new StringReader(input);

    Connection connection = DriverManager.getConnection(
            "jdbc:postgresql://db_host:5432/db_name", "user",
            "password");

    CopyManager copyManager = connection.unwrap(PGConnection.class).getCopyAPI();

    String copyCommand = "COPY my_table (addresses) " + 
                         "FROM STDIN WITH (" + 
                           "DELIMITER '\t', " + 
                           "FORMAT csv, " + 
                           "NULL '\\N', " + 
                           "ESCAPE '\"', " +
                           "QUOTE '\"')";

    copyManager.copyIn(copyCommand, reader);

執行此程序會產生以下異常:

Exception in thread "main" org.postgresql.util.PSQLException: ERROR: malformed record literal: "(10.10.10.1"
  Detail: Unexpected end of input.
  Where: COPY only_address, line 1, column addresses: "{(10.10.10.1,80),(10.10.10.2,443)}"
    at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2422)
    at org.postgresql.core.v3.QueryExecutorImpl.processCopyResults(QueryExecutorImpl.java:1114)
    at org.postgresql.core.v3.QueryExecutorImpl.endCopy(QueryExecutorImpl.java:963)
    at org.postgresql.core.v3.CopyInImpl.endCopy(CopyInImpl.java:43)
    at org.postgresql.copy.CopyManager.copyIn(CopyManager.java:185)
    at org.postgresql.copy.CopyManager.copyIn(CopyManager.java:160)

我已嘗試在輸入中使用括號的不同組合,但似乎無法使COPY正常工作。 我可能會出錯的任何想法?

有關JUnit測試的項目,請參閱https://git.mikael.io/mikaelhg/pg-object-csv-copy-poc/ ,該項目可以滿足您的需求。

基本上,您希望能夠將逗號用於兩件事:分隔數組項和分隔類型字段,但您不希望CSV解析將逗號解釋為字段描述符。

所以

  1. 您想告訴CSV解析器將整行視為一個字符串,一個字段,您可以通過將其括在單引號中並告訴CSV解析器來完成此操作,並且
  2. 您希望PG字段解析器將每個數組項類型實例視為包含在雙引號中。

碼:

copyManager.copyIn("COPY my_table (addresses) FROM STDIN WITH CSV QUOTE ''''", reader);

DML示例1:

COPY my_table (addresses) FROM STDIN WITH CSV QUOTE ''''

CSV示例1:

'{"(10.0.0.1,1)","(10.0.0.2,2)"}'
'{"(10.10.10.1,80)","(10.10.10.2,443)"}'
'{"(10.10.10.3,8080)","(10.10.10.4,4040)"}'

DML示例2,轉義雙引號:

COPY my_table (addresses) FROM STDIN WITH CSV

CSV示例2,轉義雙引號:

"{""(10.0.0.1,1)"",""(10.0.0.2,2)""}"
"{""(10.10.10.1,80)"",""(10.10.10.2,443)""}"
"{""(10.10.10.3,8080)"",""(10.10.10.4,4040)""}"

完整的JUnit測試類:

package io.mikael.poc;

import com.google.common.io.CharStreams;
import org.junit.*;
import org.postgresql.PGConnection;
import org.postgresql.copy.CopyManager;
import org.testcontainers.containers.PostgreSQLContainer;

import java.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;

import static java.nio.charset.StandardCharsets.UTF_8;

public class CopyTest {

    private Reader reader;

    private Connection connection;

    private CopyManager copyManager;

    private static final String CREATE_TYPE = "CREATE TYPE address AS (ip inet, port int)";

    private static final String CREATE_TABLE = "CREATE TABLE my_table (addresses  address[] NULL)";

    private String loadCsvFromFile(final String fileName) throws IOException {
        try (InputStream is = getClass().getResourceAsStream(fileName)) {
            return CharStreams.toString(new InputStreamReader(is, UTF_8));
        }
    }

    @ClassRule
    public static PostgreSQLContainer db = new PostgreSQLContainer("postgres:10-alpine");

    @BeforeClass
    public static void beforeClass() throws Exception {
        Class.forName("org.postgresql.Driver");
    }

    @Before
    public void before() throws Exception {
        String input = loadCsvFromFile("/data_01.csv");
        reader = new StringReader(input);

        connection = DriverManager.getConnection(db.getJdbcUrl(), db.getUsername(), db.getPassword());
        copyManager = connection.unwrap(PGConnection.class).getCopyAPI();

        connection.setAutoCommit(false);
        connection.beginRequest();

        connection.prepareCall(CREATE_TYPE).execute();
        connection.prepareCall(CREATE_TABLE).execute();
    }

    @After
    public void after() throws Exception {
        connection.rollback();
    }

    @Test
    public void copyTest01() throws Exception {
        copyManager.copyIn("COPY my_table (addresses) FROM STDIN WITH CSV QUOTE ''''", reader);

        final StringWriter writer = new StringWriter();
        copyManager.copyOut("COPY my_table TO STDOUT WITH CSV", writer);
        System.out.printf("roundtrip:%n%s%n", writer.toString());

        final ResultSet rs = connection.prepareStatement(
                "SELECT array_to_json(array_agg(t)) FROM (SELECT addresses FROM my_table) t")
                .executeQuery();
        rs.next();
        System.out.printf("json:%n%s%n", rs.getString(1));
    }

}

測試輸出:

roundtrip:
"{""(10.0.0.1,1)"",""(10.0.0.2,2)""}"
"{""(10.10.10.1,80)"",""(10.10.10.2,443)""}"
"{""(10.10.10.3,8080)"",""(10.10.10.4,4040)""}"

json:
[{"addresses":[{"ip":"10.0.0.1","port":1},{"ip":"10.0.0.2","port":2}]},{"addresses":[{"ip":"10.10.10.1","port":80},{"ip":"10.10.10.2","port":443}]},{"addresses":[{"ip":"10.10.10.3","port":8080},{"ip":"10.10.10.4","port":4040}]}]

CSV格式中,當您指定分隔符時,您不能將其用作數據中的字符,除非您將其轉義!

使用逗號作為分隔符的csv文件示例

正確的記錄: data1, data2解析結果: [0] => data1 [1] => data2

不正確的一個: data,1, data2解析結果: [0] => data [1] => 1 [2] => data2

最后你不需要將文件作為csv加載,而是作為一個簡單的文件,所以替換你的方法loadCsvFromFile(); 通過

public String loadRecordsFromFile(File file) {
 LineIterator it = FileUtils.lineIterator(file, "UTF-8");
 StringBuilder sb = new StringBuilder();
 try {
   while (it.hasNext()) {
     sb.append(it.nextLine()).append(System.nextLine);
   }
 } 
 finally {
   LineIterator.closeQuietly(iterator);
 }

 return sb.toString();
}

不要忘記在pom文件中添加此依賴項

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->

    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>

或者從commons.apache.org下載JAR

1NF

首先,我認為您的表設計錯誤,因為它不符合1NF標准。 每個字段應該只包含原子屬性,但事實並非如此。 為什么不像這樣的表:

CREATE TABLE my_table (
    id,
    ip inet,
    port int
)

其中id是源文件中的行號, ip / port是該行中的一個地址? 樣本數據:

id | ip         | port
-----------------------
1  | 10.10.10.1 | 80
1  | 10.10.10.2 | 443
2  | 10.10.10.3 | 8080
2  | 10.10.10.4 | 4040
...

因此,您將能夠在單個地址上查詢您的數據庫(查找所有關聯的地址,如果兩個地址位於同一行,則返回true,無論您想要什么......)。

加載數據

但是我們假設你知道自己在做什么。 這里的主要問題是您的輸入數據文件是特殊格式。 它可能是一個單列CSV文件,但它將是一個非常簡並的CSV文件。 無論如何,在將行插入數據庫之前,必須先對這些行進行轉換。 您有兩種選擇:

  1. 你讀取輸入文件的每一行並進行INSERT (這可能需要一段時間);
  2. 您將輸入文件轉換為具有預期格式的文本文件並使用COPY

逐個插入

第一個選項看起來很簡單:對於csv文件的第一行{(10.10.10.1,80),(10.10.10.2,443)} ,您必須運行查詢:

INSERT INTO my_table VALUES (ARRAY[('10.10.10.1',80),('10.10.10.2',443)]::address[], 4)

為此,您只需創建一個新字符串:

String value = row.replaceAll("\\{", "ARRAY[")
                    .replaceAll("\\}", "]::address[]")
                    .replaceAll("\\(([0-9.]+),", "'$1'");
String sql = String.format("INSERT INTO my_table VALUES (%s)", value);

並為輸入文件的每一行執行查詢(或者為了更好的安全性,請使用預准備語句 )。

插入COPY

我將詳細說明第二種選擇。 您必須在Java代碼中使用:

copyManager.copyIn(sql, from);

復制查詢是COPY FROM STDIN語句,而from讀者。 聲明如下:

COPY my_table (addresses) FROM STDIN WITH (FORMAT text);

要提供副本管理器,您需要數據(請注意引號):

{"(10.10.10.1,80)","(10.10.10.2,443)"}
{"(10.10.10.3,8080)","(10.10.10.4,4040)"}

用一個臨時文件

以正確格式獲取數據的更簡單方法是創建臨時文件。 您讀取輸入文件的每一行並替換(通過"() by )" 將此處理的行寫入臨時文件。 然后將此文件上的讀者傳遞給副本管理器。

在飛行中

使用兩個線程您可以使用兩個線程:

  • 線程1讀取輸入文件,逐個處理這些行並將它們寫入PipedWriter

  • 線程2將連接到前一個PipedWriterPipedReader傳遞給復制管理器。

主要的困難是以線程2開始將數據寫入PipedWriter之前線程2開始讀取PipedReader的方式同步線程。 以我的這個項目為例。

使用自定義閱讀器 from閱讀器可以是類似(天真版本)的實例:

class DataReader extends Reader {
    PushbackReader csvFileReader;
    private boolean wasParenthese;

    public DataReader(Reader csvFileReader) {
        this.csvFileReader = new PushbackReader(csvFileReader, 1);
        wasParenthese = false;
    }

    @Override
    public void close() throws IOException {
        this.csvFileReader.close();
    }

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
        // rely on read()
        for (int i = off; i < off + len; i++) {
            int c = this.read();
            if (c == -1) {
                return i-off > 0 ? i-off : -1;
            }
            cbuf[i] = (char) c;
        }
        return len;
    }

    @Override
    public int read() throws IOException {
        final int c = this.csvFileReader.read();
        if (c == '(' && !this.wasParenthese) {
            this.wasParenthese = true;
            this.csvFileReader.unread('(');
            return '"'; // add " before (
        } else {
            this.wasParenthese = false;
            if (c == ')') {
                this.csvFileReader.unread('"');
                return ')';  // add " after )
            } else {
                return c;
            }
        }
    }
}

(這是一個天真的版本,因為正確的方法是只覆蓋public int read(char[] cbuf, int off, int len) 。但是你應該處理cbuf來添加引號並存儲額外的字符向右推:這有點單調乏味。 現在,如果r是該文件的讀者:

{(10.10.10.1,80),(10.10.10.2,443)}
{(10.10.10.3,8080),(10.10.10.4,4040)}

只需使用:

Class.forName("org.postgresql.Driver");
Connection connection = DriverManager
        .getConnection("jdbc:postgresql://db_host:5432/db_base", "user", "passwd");

CopyManager copyManager = connection.unwrap(PGConnection.class).getCopyAPI();
copyManager.copyIn("COPY my_table FROM STDIN WITH (FORMAT text)", new DataReader(r));

在批量加載

如果要加載大量數據,請不要忘記基本提示 :禁用自動提交,刪除索引和約束,並使用TRUNCATEANALYZE ,如下所示:

TRUNCATE my_table;
COPY ...;
ANALYZE my_table;

這將加快裝載速度。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM