web-dev-qa-db-ja.com

JDBCを使用したカスタムタイプの配列を含むPostgresへのCSVコピー

私のデータベースで定義されているカスタムタイプがあります

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が機能していないようです。私が間違っているかもしれないアイデアはありますか?

18
Swaranga Sarma

希望どおりのJUnitテストを行うプロジェクトについては、 https://git.mikael.io/mikaelhg/pg-object-csv-copy-poc/ を参照してください。

基本的に、2つのことにカンマを使用できるようにする必要があります。配列項目を分離することと、型フィールドを分離することですが、CSV解析でコンマをフィールド区切り文字として解釈しないでください。

そう

  1. 行全体を1つの文字列、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}]}]
4
Mikael Gueck

に 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>
_

または、JARを commons.Apache.org からダウンロードします

1
Halayem Anis

1NF

まず、テーブルのデザインは 1NF に準拠していないため、間違っていると思います。すべてのフィールドにアトミック属性のみを含める必要がありますが、そうではありません。なぜ次のようなテーブルではないのですか?

_CREATE TABLE my_table (
    id,
    ip inet,
    port int
)
_

ここで、idはソースファイルの行番号であり、ip/portはこの行のアドレスの1つですか。サンプルデータ:

_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
...
_

したがって、単一のアドレスでデータベースにクエリを実行できます(関連付けられているすべてのアドレスを検索し、2つのアドレスが同じ行にある場合は、他に何が必要であっても...を返します)。

データを読み込む

しかし、あなたがあなたが何をしているか知っていると仮定しましょう。ここでの主な問題は、入力データファイルが特別な形式であることです。単一列のCSVファイルの場合もありますが、非常に縮退したCSVファイルになります。とにかく、行をデータベースに挿入する前に、行を変換する必要があります。次の2つのオプションがあります。

  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で挿入

2番目のオプションについて詳しく説明します。 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)"}
_

一時ファイルあり

データを正しい形式で取得する最も簡単な方法は、一時ファイルを作成することです。入力ファイルの各行を読み取り、_(_を_"(_で置き換え、_)_を_)"_で置き換えます。この処理された行を一時ファイルに書き込みます。次に、このファイルのリーダーをコピーマネージャに渡します。

急いで

2つのスレッドを使用する2つのスレッドを使用できます。

  • スレッド1は、入力ファイルを読み取り、行を1つずつ処理して、PipedWriterに書き込みます。

  • スレッド2は、以前のPipedReaderに接続されたPipedWriterをコピーマネージャーに渡します。

主な問題は、スレッド1がPipedReaderへのデータの書き込みを開始する前に、スレッド2がPipedWriterの読み取りを開始するようにスレッドを同期させることです。例は この私のプロジェクト を参照してください。

カスタムリーダー付き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;
_

これにより、読み込みが速くなります。

0
jferard