Java 使用JDBC将CSV复制到带有自定义类型数组的Postgres
我在数据库中有一个自定义类型定义为Java 使用JDBC将CSV复制到带有自定义类型数组的Postgres,java,database,postgresql,jdbc,postgresql-9.5,Java,Database,Postgresql,Jdbc,Postgresql 9.5,我在数据库中有一个自定义类型定义为 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)} 我使用以下代码段执行复制: Class.
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)}
我使用以下代码段执行复制:
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)
我尝试了输入中括号的不同组合,但似乎无法使副本正常工作。你知道我哪里出了问题吗?请参阅一个带有JUnit测试的项目,该测试可以满足你的需要
基本上,您希望能够将逗号用于两件事:分隔数组项和分隔类型字段,但不希望CSV解析将逗号解释为字段描述符
所以
<> LI>你想告诉CSV解析器把整个行看作一个字符串,一个字段,你可以把它用单引号括起来,告诉CSV解析器,和。
<> LI>希望PG字段解析器考虑每个数组项类型实例,以双引号括起来。
代码:
DML示例1:
COPY my_table (addresses) FROM STDIN WITH CSV QUOTE ''''
'{"(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)"}'
CSV示例1:
COPY my_table (addresses) FROM STDIN WITH CSV QUOTE ''''
'{"(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
"{""(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)""}"
CSV示例2,转义双引号:
COPY my_table (addresses) FROM STDIN WITH CSV
"{""(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>
公地io
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文件。无论如何,在将这些行插入数据库之前,必须对它们进行转换。您有两个选择:
您读取输入文件的每一行,然后进行插入(这可能需要一段时间)李>
您可以将输入文件转换为预期格式的文本文件,并使用COPY
逐一插入
第一个选项似乎很简单:对于csv文件的第一行,{(10.10.10.1,80),(10.10.10.2443)}
,您必须运行以下查询:
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);
并对输入文件的每一行执行查询(或者为了更好的安全性,使用)
插入并复制
我将详细阐述第二种选择。您必须在Java代码中使用:
copyManager.copyIn(sql, from);
其中,复制查询是一个从STDIN
语句复制,而从
是一个读卡器。声明将是:
COPY my_table (addresses) FROM STDIN WITH (FORMAT text);
要向复制管理器提供数据,您需要以下数据(请注意引号):
用临时文件
以正确格式获取数据的更简单方法是创建一个临时文件。您可以读取输入文件的每一行,并将(
替换为”(
和)
替换为)“
。将已处理的行写入临时文件。然后将此文件的读取器传递给复制管理器
在飞行中
有两个线程
您可以使用两个线程:
- 线程1读取输入文件,逐个处理这些行,并将它们写入
PipedWriter
- 线程2将连接到上一个
PipedWriter
的PipedWriter
传递到复制管理器
主要的困难是同步线程,使线程2在线程1开始将数据写入PipedWriter
之前开始读取PipedWriter
。有关示例,请参见
带有自定义读卡器
来自
阅读器的可以是类似(原始版本)的实例:
只需使用:
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;
这将加快加载速度。使用复制是一项硬要求,或者即使速度较慢,使用插入也可以吗?@Gregoyarenius根据我们的吞吐量和成本分析,我们确实需要支持复制。您是否尝试转储(pg\u dump
)检查现有数据并检查CSV语法和PostgreSQL生成的复制命令?CSV文件的确切形状是什么<代码>{(10.10.10.1,80),(10.10.10.2443){(10.10.10.38080),(10.10.10.44040)}
不是CSV:)@jbet在两个结构之间有一行新的'\n'
:{(10.10.10.1,80),(10.10.10.2443)}\n{(10.10.10.10.38080),(10.10.10.44040)