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)