[英]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解析將逗號解釋為字段描述符。
所以
碼:
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標准。 每個字段應該只包含原子屬性,但事實並非如此。 為什么不像這樣的表:
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文件。 無論如何,在將行插入數據庫之前,必須先對這些行進行轉換。 您有兩種選擇:
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
我將詳細說明第二種選擇。 您必須在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將連接到前一個PipedWriter
的PipedReader
傳遞給復制管理器。
主要的困難是以線程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));
如果要加載大量數據,請不要忘記基本提示 :禁用自動提交,刪除索引和約束,並使用TRUNCATE
和ANALYZE
,如下所示:
TRUNCATE my_table;
COPY ...;
ANALYZE my_table;
這將加快裝載速度。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.