我在我的数据库中定义了一个自定义类型
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正常工作。我可能会出错的任何想法?
答案 0 :(得分:4)
有关具有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}]}]
答案 1 :(得分:1)
以 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
答案 2 :(得分:0)
首先,我认为您的表格设计错误,因为它不符合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);
并对输入文件的每一行执行查询(或为了更好的安全性,请使用prepared statement)。
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)"}
以正确格式获取数据的简单方法是创建临时文件。您阅读输入文件的每一行,并将(
替换为"(
,将)
替换为)"
。将此处理的行写入临时文件。然后将此文件的读者传递给副本管理器。
使用两个帖子 您可以使用两个线程:
线程1读取输入文件,逐个处理这些行并将它们写入PipedWriter
。
主题2将连接到上一个PipedReader
的{{1}}传递给副本管理器。
主要的困难是以线程2开始将数据写入PipedWriter
之前线程2开始读取PipedReader
的方式同步线程。有关示例,请参阅this project of mine。
使用自定义阅读器
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)}
如果要加载大量数据,请不要忘记the basic tips:禁用自动提交,删除索引和约束,并使用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
这会加快装载速度。