Vor einiger Zeit musste ich größere Listen von Daten abarbeiten und die Ergebnisse zwischenspeichern. An mehreren Stellen im Code hatte ich dann Batch-Schleifen, die eine Liste in Batches á N Stück aufteilten und diese verarbeiteten. Die Gelegenheit, das etwas generischer zu Formulieren.
Die Grundstruktur einer Batch-weisen Verarbeitung einer Liste von Elementen sieht wie folgt aus:
List data = ...
final int batchSize = 50;
int fromOffset = 0;
do {
int toOffset = Math.min(data.size(), fromOffset + batchSize);
List batch = data.subList(fromOffset, toOffset);
// verarbeite Batch
fromOffset = toOffset;
} while (fromOffset < data.size());
Formuliert man das ganze mit einem Context-Objekt, in dem Informationen zum aktuellen Batch stehen, sowie ein Interface für Konsumenten, erhält man eine generische Batch-Abarbeitung:
@Log4j
public class Batch {
public interface Consumer<T> {
void consume(List<T> batch, Context context);
}
@Getter
@AllArgsConstructor
@Builder
@EqualsAndHashCode
public static class Context {
private int currentBlockSize;
private int blockSize;
private int currentOffset;
}
public static <T> void process(List<T> data, int blockSize, Consumer<T> consumer) {
final int maxOffset = data.size();
int fromOffset = 0;
do {
int currentBlockSize = Math.min(blockSize, maxOffset - fromOffset);
int toOffset = fromOffset + currentBlockSize;
log.info("Verarbeite Block {} bis {} von {}", fromOffset, toOffset, maxOffset);
consumer.consume(data.subList(fromOffset, toOffset),
Context.builder()
.currentOffset(fromOffset)
.currentBlockSize(currentBlockSize)
.blockSize(blockSize)
.build());
fromOffset = toOffset;
} while (fromOffset < maxOffset);
}
}
Die Nutzung ist relativ einfach:
@Test
public void process() {
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> receivedBatchEntries = new ArrayList<>();
List<Batch.Context> contexts = new ArrayList<>();
Batch.process(ints, 3, (batch, context) -> {
receivedBatchEntries.addAll(batch);
contexts.add(context);
});
assertThat(receivedBatchEntries, contains(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
assertThat(contexts, contains(
Batch.Context.builder().blockSize(3).currentBlockSize(3).currentOffset(0).build(),
Batch.Context.builder().blockSize(3).currentBlockSize(3).currentOffset(3).build(),
Batch.Context.builder().blockSize(3).currentBlockSize(3).currentOffset(6).build(),
// Letzter lauf hat nur ein Element
Batch.Context.builder().blockSize(3).currentBlockSize(1).currentOffset(9).build()
));
}
Das Ganze kann man dann auch auf asynchrone und parallele Abarbeitungen erweitern.