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
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.
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 .
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.
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()) {
...
}
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é!
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);
}
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.