18 votes

Copie de CSV vers Postgres avec un tableau de type personnalisé en utilisant JDBC

J'ai un type personnalisé défini dans ma base de données comme étant

CREATE TYPE address AS (ip inet, port int);

Et un tableau qui utilise ce type dans un tableau :

CREATE TABLE my_table (
  addresses  address[] NULL
)

J'ai un exemple de fichier CSV avec le contenu suivant

{(10.10.10.1,80),(10.10.10.2,443)}
{(10.10.10.3,8080),(10.10.10.4,4040)}

Et j'utilise l'extrait de code suivant pour effectuer ma COPIE :

    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);

L'exécution de ce programme produit l'exception suivante :

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)

J'ai essayé différentes combinaisons de parenthèses dans l'entrée mais je n'arrive pas à faire fonctionner la COPIE. Avez-vous une idée de ce que je pourrais faire de mal ?

4voto

mikaelhg Points 647

Voir https://git.mikael.io/mikaelhg/pg-object-csv-copy-poc/ pour un projet avec un test JUnit qui fait ce que vous voulez.

En fait, vous voulez pouvoir utiliser les virgules pour deux choses : pour séparer les éléments d'un tableau et pour séparer les champs de type, mais vous ne voulez pas que l'analyse CSV interprète les virgules comme des délimiteurs de champ.

Alors

  1. vous voulez indiquer à l'analyseur CSV de considérer la ligne entière comme une seule chaîne, un seul champ, ce que vous pouvez faire en la mettant entre guillemets simples et en l'indiquant à l'analyseur CSV, et
  2. vous voulez que l'analyseur de champ PG considère que chaque instance de type d'élément de tableau doit être entourée d'un guillemet double.

Code :

copyManager.copyIn("COPY my_table (addresses) FROM STDIN WITH CSV QUOTE ''''", reader);

Exemple DML 1 :

COPY my_table (addresses) FROM STDIN WITH CSV QUOTE ''''

Exemple 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 exemple 2, en échappant les doubles quotes :

COPY my_table (addresses) FROM STDIN WITH CSV

CSV exemple 2, en échappant les guillemets doubles :

"{""(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)""}"

Classe de test JUnit complète :

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));
    }

}

Sortie de test :

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}]}]

1voto

Halayem Anis Points 171

En CSV lorsque vous spécifiez un séparateur, vous ne pouvez pas l'utiliser comme caractère dans vos données, sauf si vous l'échappez !

exemple d'un fichier csv utilisant la virgule comme séparateur

un enregistrement correct : data1, data2   analyser les résultats : [0] => data1 [1] => data2

une erreur : data,1, data2 analyser les résultats : [0] => data [1] => 1 [2] => data2

enfin vous n'avez pas besoin de charger votre fichier comme un csv, mais comme un simple fichier, donc remplacez votre méthode loadCsvFromFile(); par

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();
}

N'oubliez pas d'ajouter cette dépendance dans votre fichier pom.

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->

    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>

Ou pour télécharger le JAR à partir de commons.apache.org

0voto

jferard Points 1509

1NF

Tout d'abord, je pense que la conception de votre table est mauvaise car elle n'est pas 1NF conforme. Chaque champ ne devrait contenir que des attributs atomiques, mais ce n'est pas le cas. Pourquoi pas un tableau comme :

CREATE TABLE my_table (
    id,
    ip inet,
    port int
)

Dónde id est le numéro de votre ligne dans le fichier source et ip / port une des adresses de cette ligne ? Exemple de données :

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
...

Ainsi, vous pourrez interroger votre base de données sur une seule adresse (trouver toutes les adresses associées, retourner vrai si deux adresses sont sur la même ligne, tout ce que vous voulez...).

Charger les données

Mais supposons que vous savez ce que vous faites. Le principal problème ici est que votre fichier de données d'entrée est dans un format spécial. Il peut s'agir d'un fichier CSV à une seule colonne, mais ce serait un fichier CSV très dégénéré. Quoi qu'il en soit, vous devez transformer les lignes avant de les insérer dans la base de données. Vous avez deux possibilités :

  1. vous lisez chaque ligne du fichier d'entrée et vous faites un INSERT (cela peut prendre un certain temps) ;
  2. vous convertissez le fichier d'entrée en un fichier texte avec le format attendu et utilisez COPY .

Insérer un par un

La première option semble facile : pour la première ligne du fichier csv, {(10.10.10.1,80),(10.10.10.2,443)} vous devez exécuter la requête :

INSERT INTO my_table VALUES (ARRAY[('10.10.10.1',80),('10.10.10.2',443)]::address[], 4)

Pour ce faire, il suffit de créer une nouvelle chaîne :

String value = row.replaceAll("\\{", "ARRAY[")
                    .replaceAll("\\}", "]::address[]")
                    .replaceAll("\\(([0-9.]+),", "'$1'");
String sql = String.format("INSERT INTO my_table VALUES (%s)", value);

Et exécutez la requête pour chaque ligne du fichier d'entrée (ou pour une meilleure sécurité, utilisez un fichier déclaration préparée ).

Insertion avec COPY

Je vais développer la deuxième option. Vous devez l'utiliser dans le code Java :

copyManager.copyIn(sql, from);

Où la requête de copie est une COPY FROM STDIN et from est un lecteur. La déclaration sera :

COPY my_table (addresses) FROM STDIN WITH (FORMAT text);

Pour alimenter le copy manager, vous avez besoin de données comme (notez les guillemets) :

{"(10.10.10.1,80)","(10.10.10.2,443)"}
{"(10.10.10.3,8080)","(10.10.10.4,4040)"}

Avec un fichier temporaire

La façon la plus simple d'obtenir les données dans le bon format est de créer un fichier temporaire. Vous lisez chaque ligne du fichier d'entrée et remplacez ( par "( y ) par )" . Écrivez cette ligne traitée dans un fichier temporaire. Passez ensuite un lecteur sur ce fichier au gestionnaire de copie.

A la volée

Avec deux fils Vous pouvez utiliser deux fils :

  • Le thread 1 lit le fichier d'entrée, traite les lignes une par une et les écrit dans un fichier de type PipedWriter .

  • Le fil 2 passe un PipedReader connecté à l'ancien PipedWriter au responsable de la copie.

La principale difficulté consiste à synchroniser les threads de manière à ce que le thread 2 commence à lire le code de l'utilisateur. PipedReader avant que le thread 1 ne commence à écrire des données dans le PipedWriter . Voir ce projet qui est le mien pour un exemple.

Avec un lecteur personnalisé Le site from Le lecteur pourrait être une instance de quelque chose comme (version naïve) :

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;
            }
        }
    }
}

(Il s'agit d'une version naïve, car la bonne façon de procéder serait de remplacer seulement public int read(char[] cbuf, int off, int len) . Mais vous devez ensuite traiter le cbuf pour ajouter les guillemets et stocker les caractères supplémentaires poussés vers la droite : c'est un peu fastidieux). Maintenant, si r est le lecteur du fichier :

{(10.10.10.1,80),(10.10.10.2,443)}
{(10.10.10.3,8080),(10.10.10.4,4040)}

Il suffit d'utiliser :

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));

Sur le chargement en vrac

Si vous chargez une énorme quantité de données, n'oubliez pas les conseils de base : désactivez l'autocommit, supprimez les index et les contraintes, et utilisez la fonction TRUNCATE y ANALYZE comme suit :

TRUNCATE my_table;
COPY ...;
ANALYZE my_table;

Cela accélérera le chargement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X