123 votes

La conversion la plus efficace de ResultSet en JSON?

Le code suivant convertit un ResultSet en une chaîne JSON en utilisant JSONArray et JSONObject.

import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;

import java.sql.SQLException;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;

public class ResultSetConverter {
  public static JSONArray convert( ResultSet rs )
    throws SQLException, JSONException
  {
    JSONArray json = new JSONArray();
    ResultSetMetaData rsmd = rs.getMetaData();

    while(rs.next()) {
      int numColumns = rsmd.getColumnCount();
      JSONObject obj = new JSONObject();

      for (int i=1; i

`

  • Y a-t-il un moyen plus rapide?
  • Y a-t-il un moyen d'utiliser moins de mémoire?

`

1 votes

1 votes

L'implémentation pour java.sql.Types.ARRAY n'a pas fonctionné pour moi en utilisant postgresql (le tableau a été mis en tant que chaîne en utilisant "{...}". J'ai finalement modifié la ligne "obj.put(column_name, rs.getArray(column_name));" en "Array array = rs.getArray(column_name); if ( array != null ) obj.put(column_name, new JSONArray(array.getArray()));"

0 votes

Si les performances sont un problème majeur, vous ne devriez pas utiliser cette API JSON mais plutôt utiliser une bibliothèque de streaming qui écrit simplement du JSON sans avoir besoin de créer des objets en mémoire de toutes les données (où vous pouvez rechercher/trouver des éléments dans l'arborescence). Cela dit, je m'assurerais que vous avez effectivement un problème de performances avant de faire cela.

40voto

Plap Points 346

Je pense qu'il y a un moyen d'utiliser moins de mémoire (une quantité fixe et non linéaire dépendant de la cardinalité des données) mais cela implique de changer la signature de la méthode. En fait, nous pouvons imprimer les données Json directement sur un flux de sortie dès que nous les récupérons dans le ResultSet : les données déjà écrites seront collectées puisque nous n'avons pas besoin d'un tableau qui les garde en mémoire.

J'utilise GSON qui accepte les adaptateurs de type. J'ai écrit un adaptateur de type pour convertir ResultSet en JsonArray et cela ressemble beaucoup à votre code. J'attends la version "Gson 2.1 : Targeted Dec 31, 2011" qui aura le "Support for user-defined streaming type adapters". Je modifierai alors mon adaptateur pour en faire un adaptateur de type streaming.


Mise à jour

Comme promis, je suis de retour mais pas avec Gson, plutôt avec Jackson 2. Désolé d'être en retard (de 2 ans).

Préface : La clé pour utiliser moins de mémoire du résultat lui-même se trouve dans le curseur "côté serveur". Avec ce type de curseurs (alias resultset pour les développeurs Java), le SGBD envoie les données de manière incrémentielle au client (alias driver) au fur et à mesure que le client avance dans la lecture. Je pense que le curseur Oracle est côté serveur par défaut. Pour MySQL > 5.0.2, recherchez useCursorFetch à l'adresse suivante paramètre de l'url de connexion . Vérifiez votre SGBD préféré.

1 : Donc, pour utiliser moins de mémoire, nous devons :

  • utiliser le curseur côté serveur derrière la scène
  • utiliser le jeu de résultats ouvert en tant que lecture seulement et, bien sûr, en avant seulement ;
  • éviter de charger tout le curseur dans une liste (ou une JSONArray ) mais écrire chaque ligne directement sur un ligne de sortie où par ligne de sortie, j'entends un flux de sortie ou un rédacteur ou encore un générateur json qui englobe un flux de sortie ou un rédacteur.

2 : Comme le dit Jackson Documentation :

L'API de streaming est la plus performante (frais généraux les plus bas, lecture/écriture les plus rapides ; les deux autres méthodes s'appuient sur elle)

3 : Je vois que dans votre code vous utilisez getInt, getBoolean. getFloat... de ResultSet sans wasNull . Je pense que cela peut poser des problèmes.

4 : J'ai utilisé des tableaux pour mettre en cache les pensées et éviter d'appeler les getters à chaque itération. Bien que n'étant pas un fan de la construction switch/case, je l'ai utilisé pour cela. int SQL Types .

La réponse : Pas encore entièrement testée, elle est basée sur Jackson 2.2 :

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.2.2</version>
</dependency>

Le site ResultSetSerializer indique à Jackson comment sérialiser (transformer l'objet en JSON) un ResultSet. Il utilise l'API Streaming de Jackson à l'intérieur. Voici le code d'un test :

SimpleModule module = new SimpleModule();
module.addSerializer(new ResultSetSerializer());

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(module);

[ . . . do the query . . . ]
ResultSet resultset = statement.executeQuery(query);

// Use the DataBind Api here
ObjectNode objectNode = objectMapper.createObjectNode();

// put the resultset in a containing structure
objectNode.putPOJO("results", resultset);

// generate all
objectMapper.writeValue(stringWriter, objectNode);

Et, bien sûr, le code de la classe ResultSetSerializer :

public class ResultSetSerializer extends JsonSerializer<ResultSet> {

    public static class ResultSetSerializerException extends JsonProcessingException{
        private static final long serialVersionUID = -914957626413580734L;

        public ResultSetSerializerException(Throwable cause){
            super(cause);
        }
    }

    @Override
    public Class<ResultSet> handledType() {
        return ResultSet.class;
    }

    @Override
    public void serialize(ResultSet rs, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {

        try {
            ResultSetMetaData rsmd = rs.getMetaData();
            int numColumns = rsmd.getColumnCount();
            String[] columnNames = new String[numColumns];
            int[] columnTypes = new int[numColumns];

            for (int i = 0; i < columnNames.length; i++) {
                columnNames[i] = rsmd.getColumnLabel(i + 1);
                columnTypes[i] = rsmd.getColumnType(i + 1);
            }

            jgen.writeStartArray();

            while (rs.next()) {

                boolean b;
                long l;
                double d;

                jgen.writeStartObject();

                for (int i = 0; i < columnNames.length; i++) {

                    jgen.writeFieldName(columnNames[i]);
                    switch (columnTypes[i]) {

                    case Types.INTEGER:
                        l = rs.getInt(i + 1);
                        if (rs.wasNull()) {
                            jgen.writeNull();
                        } else {
                            jgen.writeNumber(l);
                        }
                        break;

                    case Types.BIGINT:
                        l = rs.getLong(i + 1);
                        if (rs.wasNull()) {
                            jgen.writeNull();
                        } else {
                            jgen.writeNumber(l);
                        }
                        break;

                    case Types.DECIMAL:
                    case Types.NUMERIC:
                        jgen.writeNumber(rs.getBigDecimal(i + 1));
                        break;

                    case Types.FLOAT:
                    case Types.REAL:
                    case Types.DOUBLE:
                        d = rs.getDouble(i + 1);
                        if (rs.wasNull()) {
                            jgen.writeNull();
                        } else {
                            jgen.writeNumber(d);
                        }
                        break;

                    case Types.NVARCHAR:
                    case Types.VARCHAR:
                    case Types.LONGNVARCHAR:
                    case Types.LONGVARCHAR:
                        jgen.writeString(rs.getString(i + 1));
                        break;

                    case Types.BOOLEAN:
                    case Types.BIT:
                        b = rs.getBoolean(i + 1);
                        if (rs.wasNull()) {
                            jgen.writeNull();
                        } else {
                            jgen.writeBoolean(b);
                        }
                        break;

                    case Types.BINARY:
                    case Types.VARBINARY:
                    case Types.LONGVARBINARY:
                        jgen.writeBinary(rs.getBytes(i + 1));
                        break;

                    case Types.TINYINT:
                    case Types.SMALLINT:
                        l = rs.getShort(i + 1);
                        if (rs.wasNull()) {
                            jgen.writeNull();
                        } else {
                            jgen.writeNumber(l);
                        }
                        break;

                    case Types.DATE:
                        provider.defaultSerializeDateValue(rs.getDate(i + 1), jgen);
                        break;

                    case Types.TIMESTAMP:
                        provider.defaultSerializeDateValue(rs.getTime(i + 1), jgen);
                        break;

                    case Types.BLOB:
                        Blob blob = rs.getBlob(i);
                        provider.defaultSerializeValue(blob.getBinaryStream(), jgen);
                        blob.free();
                        break;

                    case Types.CLOB:
                        Clob clob = rs.getClob(i);
                        provider.defaultSerializeValue(clob.getCharacterStream(), jgen);
                        clob.free();
                        break;

                    case Types.ARRAY:
                        throw new RuntimeException("ResultSetSerializer not yet implemented for SQL type ARRAY");

                    case Types.STRUCT:
                        throw new RuntimeException("ResultSetSerializer not yet implemented for SQL type STRUCT");

                    case Types.DISTINCT:
                        throw new RuntimeException("ResultSetSerializer not yet implemented for SQL type DISTINCT");

                    case Types.REF:
                        throw new RuntimeException("ResultSetSerializer not yet implemented for SQL type REF");

                    case Types.JAVA_OBJECT:
                    default:
                        provider.defaultSerializeValue(rs.getObject(i + 1), jgen);
                        break;
                    }
                }

                jgen.writeEndObject();
            }

            jgen.writeEndArray();

        } catch (SQLException e) {
            throw new ResultSetSerializerException(e);
        }
    }
}

0 votes

Merci. Juste une petite note: Dans Types.TIMESTAMP, il devrait être provider.defaultSerializeDateValue(rs.getTimestamp(i + 1), jgen);

27voto

oravecz Points 416

Deux choses qui rendront cela plus rapide sont :

Déplacez votre appel à rsmd.getColumnCount() hors de la boucle while. Le nombre de colonnes ne doit pas varier d'une ligne à l'autre.

Pour chaque type de colonne, vous finissez par appeler quelque chose comme ceci :

obj.put(column_name, rs.getInt(column_name));

Il sera légèrement plus rapide d'utiliser l'indice de la colonne pour récupérer la valeur de la colonne :

obj.put(column_name, rs.getInt(i));

0 votes

Aussi définir String column_name; en dehors de la boucle while.

24voto

Andrew White Points 23508

Le compilateur JIT va probablement rendre cela assez rapide car il ne s'agit que de branches et de tests de base. Vous pourriez probablement rendre cela plus élégant avec une recherche de HashMap vers un rappel, mais je doute que cela soit plus rapide. En ce qui concerne la mémoire, cela est assez mince tel quel.

Je doute que ce code soit réellement un goulot d'étranglement critique pour la mémoire ou les performances. Avez-vous une réelle raison de chercher à l'optimiser ?

0 votes

Je mets le code source dans un framework open-source, donc je ne sais pas à quoi il servira. C'est pourquoi j'essaie de le rendre aussi efficace que possible.

1 votes

@DevinDixon : Le framework est-il disponible ? Y a-t-il quelque chose de similaire au code de votre question déjà disponible dans un dépôt open source quelque part ?

0 votes

Veuillez fournir un exemple.

10voto

Lukas Eder Points 48046

Utilisez une bibliothèque tierce pour l'export JSON

Vous pourriez utiliser jOOQ pour le travail. Vous n'avez pas besoin d'utiliser toutes les fonctionnalités de jOOQ pour profiter des extensions JDBC utiles. Dans ce cas, écrivez simplement :

String json = DSL.using(connection).fetch(resultSet).formatJSON();

Les méthodes API pertinentes utilisées sont :

Le format résultant ressemblera à ceci :

{"fields":[{"name":"field-1","type":"type-1"},
           {"name":"field-2","type":"type-2"},
           ...,
           {"name":"field-n","type":"type-n"}],
 "records":[[value-1-1,value-1-2,...,value-1-n],
            [value-2-1,value-2-2,...,value-2-n]]}

Vous pourriez également créer votre propre format assez facilement, via Result.map(RecordMapper)

Cela fait essentiellement la même chose que votre code, contournant la génération d'objets JSON, "diffusant" directement dans un StringBuilder. Je dirais que le surcoût de performance devrait être négligeable dans les deux cas, cependant.

(Avertissement : je travaille pour la société derrière jOOQ)

Utilisez plutôt les fonctionnalités SQL/JSON

Bien sûr, vous n'avez pas à utiliser votre middleware pour mapper les ResultSets JDBC en JSON. La question ne mentionne pas pour quel dialecte SQL cela doit être fait, mais beaucoup prennent en charge la syntaxe standard SQL/JSON, ou quelque chose de similaire, par exemple

Oracle

SELECT json_arrayagg(json_object(*))
FROM t

SQL Server

SELECT *
FROM t
FOR JSON AUTO

PostgreSQL

SELECT to_jsonb(array_agg(t))
FROM t

0 votes

C'est génial mais j'ai des problèmes pour analyser la chaîne résultante. Lorsque certaines des valeurs contiennent un guillemet, l'analyseur ne peut pas fonctionner correctement: je pense que les guillemets à l'intérieur des valeurs devraient être échappés (" à \") afin de créer une chaîne JSON valide. Est-ce un bogue de la fonction formatJSON()? Ou est-ce que j'ai raté quelque chose?

0 votes

@Oneiros: jOOQ devrait correctement échapper les guillemets... Il vaut mieux poser une nouvelle question (avec des détails) ou signaler un bug : github.com/jOOQ/jOOQ/issues/new

0 votes

Dans votre exemple, à quoi sert resultSet dans fetch(resultSet)? Il n'est défini nulle part. Et si je récupère le ResultSet JDBC avant fetch, quel est le but de DSL.using(connection)? Pourquoi a-t-il besoin de la connexion ? :)

7voto

Geronimo Points 6482

En plus des suggestions proposées par @Jim Cook. Une autre idée est d'utiliser un switch au lieu de if-elses :

while(rs.next()) {
  int numColumns = rsmd.getColumnCount();
  JSONObject obj = new JSONObject();

  for( int i=1; i

4 votes

Parcourir en arrière (en comparant l'index zéro) est aussi plus rapide (que de comparer l'index à une expression).

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