web-dev-qa-db-fra.com

Existe-t-il un moyen élégant de traiter un flux en morceaux?

Mon scénario exact consiste à insérer des données dans la base de données par lots, donc je veux accumuler des objets DOM puis tous les 1000, les vider.

Je l'ai implémenté en mettant du code dans l'accumulateur pour détecter la saturation puis le vidage, mais cela semble faux - le contrôle du vidage devrait provenir de l'appelant.

Je pourrais convertir le flux en une liste, puis utiliser subList de manière itérative, mais cela semble aussi maladroit.

Existe-t-il une manière intéressante de prendre des mesures tous les n éléments, puis de poursuivre le flux tout en ne traitant le flux qu'une seule fois?

43
Bohemian

L'élégance est dans l'œil du spectateur. Si cela ne vous dérange pas d'utiliser une fonction avec état dans groupingBy, vous pouvez le faire:

AtomicInteger counter = new AtomicInteger();

stream.collect(groupingBy(x->counter.getAndIncrement()/chunkSize))
    .values()
    .forEach(database::flushChunk);

Cela ne gagne pas de performances ou de points d'utilisation de la mémoire par rapport à votre solution d'origine, car cela matérialisera tout le flux avant de faire quoi que ce soit.

Si vous voulez éviter de matérialiser la liste, l'API Stream ne vous aidera pas. Vous devrez obtenir l'itérateur ou le séparateur du flux et faire quelque chose comme ceci:

Spliterator<Integer> split = stream.spliterator();
int chunkSize = 1000;

while(true) {
    List<Integer> chunk = new ArrayList<>(size);
    for (int i = 0; i < chunkSize && split.tryAdvance(chunk::add); i++){};
    if (chunk.isEmpty()) break;
    database.flushChunk(chunk);
}
14
Misha

Si vous avez une dépendance à la goyave de votre projet, vous pouvez le faire:

StreamSupport.stream(Iterables.partition(simpleList, 1000).spliterator(), false).forEach(...);

Voir https://google.github.io/guava/releases/23.0/api/docs/com/google/common/collect/Lists.html#partition-Java.util.List-int-

8
user2814648

Utiliser la bibliothèque StreamEx la solution ressemblerait à

Stream<Integer> stream = IntStream.iterate(0, i -> i + 1).boxed().limit(15);
AtomicInteger counter = new AtomicInteger(0);
int chunkSize = 4;

StreamEx.of(stream)
        .groupRuns((prev, next) -> counter.incrementAndGet() % chunkSize != 0)
        .forEach(chunk -> System.out.println(chunk));

Production:

[0, 1, 2, 3]
[4, 5, 6, 7]
[8, 9, 10, 11]
[12, 13, 14]

groupRuns accepte un prédicat qui décide si 2 éléments doivent être dans le même groupe.

Il produit un groupe dès qu'il trouve le premier élément qui ne lui appartient pas.

6
Nazarii Bardiuk

Vous pouvez créer un flux de morceaux (List<T>) d'un flux d'éléments et d'une taille de morceau donnée par

  • regroupement des éléments par l'index de bloc (indice d'élément/taille de bloc)
  • classer les morceaux par leur index
  • réduire la carte à leurs éléments ordonnés uniquement

Code:

public static <T> Stream<List<T>> chunked(Stream<T> stream, int chunkSize) {
    AtomicInteger index = new AtomicInteger(0);

    return stream.collect(Collectors.groupingBy(x -> index.getAndIncrement() / chunkSize))
            .entrySet().stream()
            .sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue);
}

Exemple d'utilisation:

Stream<Integer> stream = IntStream.range(0, 100).mapToObj(Integer::valueOf);
Stream<List<Integer>> chunked = chunked(stream, 8);
chunked.forEach(chunk -> System.out.println("Chunk: " + chunk));

Production:

Chunk: [0, 1, 2, 3, 4, 5, 6, 7]
Chunk: [8, 9, 10, 11, 12, 13, 14, 15]
Chunk: [16, 17, 18, 19, 20, 21, 22, 23]
Chunk: [24, 25, 26, 27, 28, 29, 30, 31]
Chunk: [32, 33, 34, 35, 36, 37, 38, 39]
Chunk: [40, 41, 42, 43, 44, 45, 46, 47]
Chunk: [48, 49, 50, 51, 52, 53, 54, 55]
Chunk: [56, 57, 58, 59, 60, 61, 62, 63]
Chunk: [64, 65, 66, 67, 68, 69, 70, 71]
Chunk: [72, 73, 74, 75, 76, 77, 78, 79]
Chunk: [80, 81, 82, 83, 84, 85, 86, 87]
Chunk: [88, 89, 90, 91, 92, 93, 94, 95]
Chunk: [96, 97, 98, 99]
5
Peter Walser

Comme l'a dit à juste titre Misha, l'élégance est dans l'œil du spectateur. Personnellement, je pense qu'une solution élégante serait de laisser la classe qui insère dans la base de données faire cette tâche. Similaire à un BufferedWriter. De cette façon, cela ne dépend pas de votre structure de données d'origine et peut être utilisé même avec plusieurs flux l'un après l'autre. Je ne sais pas si c'est exactement ce que vous voulez dire en ayant le code dans l'accumulateur que vous pensiez être faux. Je ne pense pas que ce soit faux, car les classes existantes comme BufferedWriter fonctionnent de cette façon. Vous avez un certain contrôle de vidage de l'appelant de cette façon en appelant flush() sur le scripteur à tout moment.

Quelque chose comme le code suivant.

class BufferedDatabaseWriter implements Flushable {
    List<DomObject> buffer = new LinkedList<DomObject>();
    public void write(DomObject o) {
        buffer.add(o);
        if(buffer.length > 1000)
            flush();
    }
    public void flush() {
        //write buffer to database and clear it
    }
}

Maintenant, votre flux est traité comme ceci:

BufferedDatabaseWriter writer = new BufferedDatabaseWriter();
stream.forEach(o -> writer.write(o));
//if you have more streams stream2.forEach(o -> writer.write(o));
writer.flush();

Si vous souhaitez travailler en multithread, vous pouvez exécuter le vidage asynchrone. La prise du flux ne peut pas aller en parallèle, mais je ne pense pas qu'il existe un moyen de compter 1000 éléments d'un flux en parallèle de toute façon.

Vous pouvez également étendre le scripteur pour permettre le réglage de la taille de la mémoire tampon dans le constructeur ou le faire implémenter AutoCloseable et l'exécuter dans un essai avec des ressources et plus encore. Les belles choses que vous avez d'un BufferedWriter.

2
findusl

Il semble que non, car créer des morceaux signifie réduire le flux et réduire les terminaisons. Si vous devez conserver la nature du flux et traiter des morceaux sans collecter toutes les données avant voici mon code (ne fonctionne pas pour les flux parallèles):

private static <T> BinaryOperator<List<T>> processChunks(Consumer<List<T>> consumer, int chunkSize) {
    return (data, element) -> {
        if (data.size() < chunkSize) {
            data.addAll(element);
            return data;
        } else {
            consumer.accept(data);
            return element; // in fact it's new data list
        }
    };
}

private static <T> Function<T, List<T>> createList(int chunkSize) {
    AtomicInteger limiter = new AtomicInteger(0);
    return element -> {
        limiter.incrementAndGet();
        if (limiter.get() == 1) {
            ArrayList<T> list = new ArrayList<>(chunkSize);
            list.add(element);
            return list;
        } else if (limiter.get() == chunkSize) {
            limiter.set(0);
        }
        return Collections.singletonList(element);
    };
}

et comment utiliser

Consumer<List<Integer>> chunkProcessor = (list) -> list.forEach(System.out::println);

    int chunkSize = 3;

    Stream.generate(StrTokenizer::getInt).limit(13)
            .map(createList(chunkSize))
            .reduce(processChunks(chunkProcessor, chunkSize))
            .ifPresent(chunkProcessor);

static Integer i = 0;

static Integer getInt()
{
    System.out.println("next");
    return i++;
}

il imprimera

suivant suivant suivant suivant 0 1 2 suivant suivant suivant 3 4 5 suivant suivant suivant 6 7 8 suivant suivant suivant 9 10 11 12

l'idée derrière est de créer des listes dans une opération de carte avec "motif"

[1 ,], [2], [3], [4 ,] ...

et fusionner (+ processus) avec réduire.

[1,2,3], [4,5,6], ...

et n'oubliez pas de traiter le dernier morceau "coupé" avec

.ifPresent(chunkProcessor);
1
Yura

La plupart des réponses ci-dessus n'utilisent pas les avantages du flux comme la sauvegarde de votre mémoire. Vous pouvez essayer d'utiliser l'itérateur pour résoudre le problème

Stream<List<T>> chunk(Stream<T> stream, int size) {
  Iterator<T> iterator = stream.iterator();
  Iterator<List<T>> listIterator = new Iterator<>() {

    public boolean hasNext() {
      return iterator.hasNext();
    }

    public List<T> next() {
      List<T> result = new ArrayList<>(size);
      for (int i = 0; i < size && iterator.hasNext(); i++) {
        result.add(iterator.next());
      }
      return result;
    }
  };
  return StreamSupport.stream(((Iterable<List<T>>) () -> listIterator).spliterator(), false);
}
0
dmitryvim