私のデータベースで定義されているカスタムタイプがあります
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/ を参照してください。
基本的に、2つのことにカンマを使用できるようにする必要があります。配列項目を分離することと、型フィールドを分離することですが、CSV解析でコンマをフィールド区切り文字として解釈しないでください。
そう
コード:
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>
_
または、JARを commons.Apache.org からダウンロードします
まず、テーブルのデザインは 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つのオプションがあります。
INSERT
を作成します(これには時間がかかる場合があります)。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));
_
大量のデータをロードする場合は、忘れないでください 基本的なヒント :自動コミットを無効にし、インデックスと制約を削除して、次のようにTRUNCATE
とANALYZE
を使用します:
_TRUNCATE my_table;
COPY ...;
ANALYZE my_table;
_
これにより、読み込みが速くなります。