web-dev-qa-db-fra.com

java.util.stream avec ResultSet

J'ai peu de tables avec une grande quantité de données (environ 100 millions d'enregistrements). Je ne peux donc pas stocker ces données en mémoire, mais j'aimerais diffuser cette classe jeu de résultats en utilisant la classe Java.util.stream Et transmettre ce flux à une autre classe. J'ai lu sur les opérateurs Stream.of Et Stream.Builder, Mais ce sont des flux mis en mémoire tampon en mémoire. Alors, y a-t-il un moyen de résoudre cette question? Merci d'avance.

PDATE # 1

D'accord j'ai googlé et trouvé jooq bibliothèque. Je ne suis pas sûr, mais il semble que cela pourrait être applicable à mon cas de test. Pour résumer, j'ai quelques tables avec une grande quantité de données. Je souhaite diffuser mon ensemble de résultats et transférer ce flux à une autre méthode. Quelque chose comme ça:

// why return Stream<String>? Because my result set has String type
private Stream<Record> writeTableToStream(DataSource dataSource, String table) {

    Stream<Record> record = null;
    try (Connection connection = dataSource.getConnection()) {
        String sql = "select * from " + table;

        try (PreparedStatement pSt = connection.prepareStatement(sql)) {
            connection.setAutoCommit(false);
            pSt.setFetchSize(5000);
            ResultSet resultSet = pSt.executeQuery();
            //
            record = DSL.using(connection)
                    .fetch(resultSet).stream();
        }
    } catch (SQLException sqlEx) {
        logger.error(sqlEx);
    }

    return record;
}

Pourriez-vous s'il vous plaît conseiller quelqu'un, suis-je sur le bon chemin? Merci.

PDATE # 2

J'ai fait quelques expériences sur jooq et pourrais dire maintenant que la décision ci-dessus ne me convient pas. Ce code record = DSL.using(connection).fetch(resultSet).stream(); prend trop de temps

37
Iurii

La première chose que vous devez comprendre est ce code comme

try (Connection connection = dataSource.getConnection()) {
    …
    try (PreparedStatement pSt = connection.prepareStatement(sql)) {
        …
        return stream;
    }
}

ne fonctionne pas car au moment où vous quittez les blocs try, les ressources sont fermées alors que le traitement de Stream n’a même pas commencé.

La construction de gestion des ressources "try with resources" fonctionne pour les ressources utilisées dans une portée de bloc dans une méthode, mais vous créez une méthode d'usine renvoyant une ressource. Par conséquent, vous devez vous assurer que la fermeture du flux renvoyé fermera les ressources et que l'appelant est responsable de la fermeture du Stream.


De plus, vous avez besoin d’une fonction qui produit un élément d’une seule ligne à partir de ResultSet. En supposant que vous ayez une méthode comme

Record createRecord(ResultSet rs) {
    …
}

vous pouvez créer un Stream<Record> fondamentalement comme

Stream<Record> stream = StreamSupport.stream(new Spliterators.AbstractSpliterator<Record>(
    Long.MAX_VALUE,Spliterator.ORDERED) {
        @Override
        public boolean tryAdvance(Consumer<? super Record> action) {
            if(!resultSet.next()) return false;
            action.accept(createRecord(resultSet));
            return true;
        }
    }, false);

Mais pour le faire correctement, vous devez incorporer la gestion des exceptions et la fermeture des ressources. Vous pouvez utiliser Stream.onClose Pour enregistrer une action qui sera effectuée lorsque le Stream sera fermé, mais il doit s'agir d'un Runnable qui ne peut pas lancer d'exceptions vérifiées. De même, la méthode tryAdvance n'est pas autorisée à lancer des exceptions vérifiées. Et comme nous ne pouvons pas simplement imbriquer des blocs try(…) ici, la logique du programme des exceptions de suppression émises dans close, lorsqu'il existe déjà une exception en attente, n’est pas gratuite.

Pour nous aider ici, nous introduisons un nouveau type qui peut encapsuler des opérations de fermeture pouvant générer des exceptions vérifiées et les livrer encapsulées dans une exception non contrôlée. En implémentant AutoCloseable lui-même, il peut utiliser la construction try(…) pour enchaîner les opérations de fermeture en toute sécurité:

interface UncheckedCloseable extends Runnable, AutoCloseable {
    default void run() {
        try { close(); } catch(Exception ex) { throw new RuntimeException(ex); }
    }
    static UncheckedCloseable wrap(AutoCloseable c) {
        return c::close;
    }
    default UncheckedCloseable nest(AutoCloseable c) {
        return ()->{ try(UncheckedCloseable c1=this) { c.close(); } };
    }
}

Avec cela, l'opération entière devient:

private Stream<Record> tableAsStream(DataSource dataSource, String table)
    throws SQLException {

    UncheckedCloseable close=null;
    try {
        Connection connection = dataSource.getConnection();
        close=UncheckedCloseable.wrap(connection);
        String sql = "select * from " + table;
        PreparedStatement pSt = connection.prepareStatement(sql);
        close=close.nest(pSt);
        connection.setAutoCommit(false);
        pSt.setFetchSize(5000);
        ResultSet resultSet = pSt.executeQuery();
        close=close.nest(resultSet);
        return StreamSupport.stream(new Spliterators.AbstractSpliterator<Record>(
            Long.MAX_VALUE,Spliterator.ORDERED) {
            @Override
            public boolean tryAdvance(Consumer<? super Record> action) {
                try {
                    if(!resultSet.next()) return false;
                    action.accept(createRecord(resultSet));
                    return true;
                } catch(SQLException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }, false).onClose(close);
    } catch(SQLException sqlEx) {
        if(close!=null)
            try { close.close(); } catch(Exception ex) { sqlEx.addSuppressed(ex); }
        throw sqlEx;
    }
}

Cette méthode encapsule l'opération de fermeture nécessaire pour toutes les ressources, Connection, Statement et ResultSet dans une instance de la classe d'utilitaires décrite ci-dessus. Si une exception se produit pendant l'initialisation, l'opération de fermeture est effectuée immédiatement et l'exception est remise à l'appelant. Si la construction du flux réussit, l'opération de fermeture est enregistrée via onClose.

Par conséquent, l’appelant doit s’assurer de la bonne fermeture, comme

try(Stream<Record> s=tableAsStream(dataSource, table)) {
    // stream operation
}

Notez que la livraison d'un SQLException via RuntimeException a également été ajoutée à la méthode tryAdvance. Par conséquent, vous pouvez maintenant ajouter throws SQLException À la méthode createRecord sans problèmes.

67
Holger

jOOQ

Je vais répondre à la partie jOOQ de votre question. Depuis jOOQ 3.8, il existe maintenant de nombreuses fonctionnalités supplémentaires liées à la combinaison de jOOQ avec Stream. D'autres utilisations sont aussi documentées sur cette page jOOQ .

Votre utilisation suggérée:

Vous avez essayé ceci:

Stream<Record> stream = DSL.using(connection).fetch(resultSet).stream();

En effet, cela ne fonctionne pas bien pour les ensembles de résultats volumineux car fetch(ResultSet) récupère l'intégralité de l'ensemble de résultats en mémoire puis appelle Collection.stream() = dessus.

Meilleure utilisation (paresseuse):

Au lieu de cela, vous pouvez écrire ceci:

try (Stream<Record> stream = DSL.using(connection).fetchStream(resultSet)) {
    ...
}

... qui est essentiellement pratique pour cela:

try (Cursor<Record> cursor = DSL.using(connection).fetchLazy(resultSet)) {
    Stream<Record> stream = cursor.stream();
    ...
}

Voir aussi DSLContext.fetchStream(ResultSet)

Bien sûr, vous pouvez également laisser jOOQ exécuter votre chaîne SQL plutôt que de vous débattre avec JDBC:

try (Stream<Record> stream = 
     DSL.using(dataSource)
        .resultQuery("select * from {0}", DSL.name(table)) // Prevent SQL injection
        .fetchSize(5000)
        .fetchStream()) {
    ...
}

Utilisation de l’essai avec ressources

Notez que Stream produit par jOOQ est "ressource", c’est-à-dire qu’il contient une référence à un ResultSet ouvert (et PreparedStatement). Donc, si vous voulez vraiment renvoyer ce flux en dehors de votre méthode, assurez-vous qu'il est correctement fermé!

10
Lukas Eder

Je ne connais aucune bibliothèque connue qui le fasse pour vous.

Ceci dit, cet article montre comment envelopper le jeu de résultats avec un Iterator (ResultSetIterator) et le transmettre comme premier paramètre à Spliterators.spliteratorUnknownSize() afin de créer a Spliterator .

Le Spliterator peut ensuite être utilisé par StreamSupport afin de créer un flux par dessus.

Leur implémentation suggérée de ResultSetIterator class:

public class ResultSetIterator implements Iterator {

    private ResultSet rs;
    private PreparedStatement ps;
    private Connection connection;
    private String sql;

    public ResultSetIterator(Connection connection, String sql) {
        assert connection != null;
        assert sql != null;
        this.connection = connection;
        this.sql = sql;
    }

    public void init() {
        try {
            ps = connection.prepareStatement(sql);
            rs = ps.executeQuery();

        } catch (SQLException e) {
            close();
            throw new DataAccessException(e);
        }
    }

    @Override
    public boolean hasNext() {
        if (ps == null) {
            init();
        }
        try {
            boolean hasMore = rs.next();
            if (!hasMore) {
                close();
            }
            return hasMore;
        } catch (SQLException e) {
            close();
            throw new DataAccessException(e);
        }

    }

    private void close() {
        try {
            rs.close();
            try {
                ps.close();
            } catch (SQLException e) {
                //nothing we can do here
            }
        } catch (SQLException e) {
            //nothing we can do here
        }
    }

    @Override
    public Tuple next() {
        try {
            return SQL.rowAsTuple(sql, rs);
        } catch (DataAccessException e) {
            close();
            throw e;
        }
    }
}

et alors:

public static Stream stream(final Connection connection, 
                                       final String sql, 
                                       final Object... parms) {
  return StreamSupport
                .stream(Spliterators.spliteratorUnknownSize(
                        new ResultSetIterator(connection, sql), 0), false);
}
4
alfasin

Voici l'échantillon le plus simple de AbacusUtil .

final DataSource ds = JdbcUtil.createDataSource(url, user, password);
final SQLExecutor sqlExecutor = new SQLExecutor(ds);
sqlExecutor.stream(sql, parameters);

Divulgation: Je suis le développeur de AbacusUtil.

3
user_3380739