Speichern in der Datenbank in der Stream-Pipeline

8

Gemäß der Dokumentation auf der Oracle-Website :

Nebenwirkungen von Verhaltensparametern bei Stream-Vorgängen werden im Allgemeinen nicht empfohlen, da sie häufig zu unwissentlichen Verstößen gegen die Anforderungen an die Staatenlosigkeit sowie zu anderen Sicherheitsrisiken für Threads führen können.

Umfasst dies das Speichern von Elementen des Streams in einer Datenbank?

Stellen Sie sich den folgenden (Pseudo-) Code vor:

public SavedCar saveCar(Car car) {
  SavedCar savedCar = this.getDb().save(car);
  return savedCar;
}

public List<SavedCars> saveCars(List<Car> cars) {
  return cars.stream()
           .map(this::saveCar)
           .collect(Collectors.toList());
}

Was sind die unerwünschten Auswirkungen dieser Implementierung:

public SavedCar saveCar(Car car) {
  SavedCar savedCar = this.getDb().save(car);
  return savedCar;
}

public List<SavedCars> saveCars(List<Car> cars) {
  List<SavedCars> savedCars = new ArrayList<>();
  for (Cat car : cars) {
    savedCars.add(this.saveCar(car));
  }
  return savedCars.
}
Titulum
quelle
1
Ja , das ist schlecht und unter bestimmten Umständen werden Sie Schmerzen haben.
Eugene
Wie das? Was ist der Unterschied zum Schreiben als reguläre forSchleife?
Titulum
Dies wäre offensichtlich, wenn Sie es verwenden parallelStream, würden Sie sicherlich den Transaktionskontext verlieren.
Glains
Zweifel beim Entwerfen dieses Codes - Warum gibt eine Methode, die in Ihre Datenbank schreibt, ein aktualisiertes Modell zurück? Könnte das nicht getrennt werden? Ich meine, Datenbankobjekte in einer Phase einem anderen Objekt zuzuordnen und in einer anderen Phase in die Datenbank zu schreiben.
Naman
4
In der Dokumentation heißt es, dass Nebenwirkungen „ im Allgemeinen nicht empfohlen werden “. Dann fragen Sie "Was ist mit diesem speziellen Beispiel?", Aber wenn Sie eine Antwort erhalten, in der die Probleme des spezifischen Beispiels notiert werden, sagen Sie "Aber dies ist nur ein Beispiel". Wenn es sich bei Ihrer Frage also nicht um dieses spezielle Beispiel handelt, was ist Ihre eigentliche Frage? Erwarten Sie wirklich, dass die offizielle Dokumentation für jeden hypothetischen Anwendungsfall eine Aussage macht, wenn sie bereits eine allgemeine Aussage gemacht hat?
Holger

Antworten:

4

Gemäß der Dokumentation auf der Oracle-Website [...]

Dieser Link ist für Java 8. Möglicherweise möchten Sie die Dokumentation für Java 9 (veröffentlicht im Jahr 2017) und spätere Versionen lesen, da diese diesbezüglich expliziter sind. Speziell:

Eine Stream-Implementierung lässt einen erheblichen Spielraum bei der Optimierung der Berechnung des Ergebnisses. Beispielsweise kann eine Stream-Implementierung Operationen (oder ganze Stufen) aus einer Stream-Pipeline entfernen - und daher den Aufruf von Verhaltensparametern eliminieren -, wenn nachgewiesen werden kann, dass dies das Ergebnis der Berechnung nicht beeinflusst. Dies bedeutet, dass Nebenwirkungen von Verhaltensparametern möglicherweise nicht immer ausgeführt werden und nicht berücksichtigt werden sollten, sofern nicht anders angegeben (z. B. durch die Terminaloperationen forEachund forEachOrdered). (Ein spezielles Beispiel für eine solche Optimierung finden Sie in der über den count()Vorgang dokumentierten API-Anmerkung . Weitere Informationen finden Sie im Abschnitt zu Nebenwirkungen der Dokumentation zum Stream-Paket.)

Quelle: Java 9's Javadoc für die StreamSchnittstelle .

Und auch die aktualisierte Version des von Ihnen zitierten Dokuments:

Nebenwirkungen

Nebenwirkungen von Verhaltensparametern bei Stream-Vorgängen werden im Allgemeinen nicht empfohlen, da sie häufig zu unwissentlichen Verstößen gegen die Anforderung der Staatenlosigkeit sowie zu anderen Sicherheitsrisiken für Threads führen können.
Wenn die Verhaltensparameter Nebenwirkungen haben, sofern nicht ausdrücklich angegeben, gibt es keine Garantie für :

  • die Sichtbarkeit dieser Nebenwirkungen für andere Themen;
  • dass verschiedene Operationen an dem "gleichen" Element innerhalb derselben Stream-Pipeline in demselben Thread ausgeführt werden; und
  • Diese Verhaltensparameter werden immer aufgerufen, da eine Stream-Implementierung Operationen (oder ganze Stufen) aus einer Stream-Pipeline entfernen kann, wenn nachgewiesen werden kann, dass dies das Ergebnis der Berechnung nicht beeinflusst.

Die Reihenfolge der Nebenwirkungen kann überraschend sein. Selbst wenn eine Pipeline gezwungen ist, ein Ergebnis zu erzeugen, das mit der Reihenfolge der Begegnung der Stream-Quelle übereinstimmt (z. B. IntStream.range(0,5).parallel().map(x -> x*2).toArray()muss erzeugt werden [0, 2, 4, 6, 8]), werden keine Garantien für die Reihenfolge gegeben, in der die Mapper-Funktion auf einzelne Elemente angewendet wird, oder in Welcher Thread ein Verhaltensparameter für ein bestimmtes Element ausgeführt wird.

Das Eliminieren von Nebenwirkungen kann ebenfalls überraschend sein. Mit Ausnahme von Terminaloperationen forEachundforEachOrdered können Nebenwirkungen von Verhaltensparametern nicht immer ausgeführt werden, wenn die Stream-Implementierung die Ausführung von Verhaltensparametern optimieren kann, ohne das Ergebnis der Berechnung zu beeinflussen. (Ein spezielles Beispiel finden Sie in der API-Anmerkung, die für den countVorgang dokumentiert ist .)

Quelle: Java 9's Javadoc für das java.util.streamPaket .

Alle Betonung von mir.

Wie Sie sehen können, wird in der aktuellen offiziellen Dokumentation detaillierter auf die Probleme eingegangen, die auftreten können, wenn Sie sich entscheiden, Nebenwirkungen in Ihren Stream-Vorgängen zu verwenden. Es ist auch ganz klar auf forEachund forEachOrdereddie einzigen Terminalbetrieb zu sein , wo die Ausführung von Nebenwirkungen garantiert wird (wohlgemerkt, fadenSicherheitsFragen immer noch gelten, wie die offiziellen Beispiele zeigen).


Davon abgesehen und in Bezug auf Ihren spezifischen Code und nur diesen Code:

public List<SavedCars> saveCars(List<Car> cars) {
  return cars.stream()
           .map(this::saveCar)
           .collect(Collectors.toList());
}

Ich sehe keine Streams-bezogenen Probleme mit dem Code wie er ist.

  • Der .map()Schritt wird ausgeführt, weil .collect()(eine veränderbare Reduktionsoperation , die das offizielle Dokument anstelle von Dingen empfiehlt .forEach(list::add)) auf .map()der Ausgabe beruht und da diese Ausgabe (dh saveCar()die Ausgabe) sich von ihrer Eingabe unterscheidet, kann der Stream nicht "beweisen" dass [eliding] es das Ergebnis der Berechnung nicht beeinflussen würde " .
  • Es ist nicht parallelStream()so, dass es keine Parallelitätsprobleme verursachen sollte, die zuvor nicht existierten (natürlich .parallel()kann es zu Problemen kommen , wenn jemand später eine hinzufügt - ähnlich wie wenn jemand beschlossen hat, eine forSchleife zu parallelisieren , indem er neue Threads für die inneren Berechnungen startet ).

Das bedeutet nicht, dass der Code in diesem Beispiel Good Code ™ ist. Die Sequenz .stream.map(::someSideEffect()).collect()als Möglichkeit, Nebenwirkungen für jedes Element in einer Sammlung auszuführen, sieht möglicherweise einfacher / kurzer / eleganter aus. als sein forGegenstück, und es kann manchmal sein. Wie Eugene, Holger und einige andere Ihnen sagten, gibt es jedoch bessere Möglichkeiten, dies zu erreichen.
Ein kurzer Gedanke: Die Kosten für das StreamStarten eines einfachen foroder das Iterieren eines einfachen sind nicht zu vernachlässigen, es sei denn, Sie haben viele Elemente, und wenn Sie viele Elemente haben, möchten Sie: a) wahrscheinlich keinen neuen DB-Zugriff vornehmen für jeden saveAll(List items)wäre also eine API besser; und b) wahrscheinlich nicht den Leistungseinbruch der Verarbeitung viel ertragen wollen von Elementen nacheinander, so dass Sie am Ende die Parallelisierung verwenden und dann eine ganze Reihe neuer Probleme auftreten.

walen
quelle
1
Sehen Sie, das ist die Antwort, nach der ich gesucht habe. Eine nette Erklärung mit Links zu Dokumentationen, die das Verhalten bestätigen.
Titulum
7

Das absolut einfachste Beispiel ist:

cars.stream()
    .map(this:saveCar)
    .count()

In diesem Fall wird ab Java-9 mapnicht mehr ausgeführt. da braucht man es gar nicht, um das zu wissen count.

Es gibt mehrere andere Fälle, in denen Nebenwirkungen Ihnen große Schmerzen verursachen würden. unter bestimmten Bedingungen.

Eugene
quelle
1
Ich denke, count()würde noch ausgeführt werden, aber die Implementierung kann Zwischenschritte überspringen, wenn sie das Ergebnis aus der Quelle erzeugen kann (aber es gibt viele ifs auf Implementierungsebene )
ernest_k
2
@Titulum das ist eine ganz andere Frage und hängt von der Implementierung ab; Aber ja, solche Dinge sollten wie ein Brauch im Terminalbetrieb implementiert werden Collector.
Eugene
2
@Titulum Dies wird nirgendwo dokumentiert. Dies sind Implementierungsdetails. Aber wenn Sie die Dokumentation befolgen (wie Ihr Teil mit Nebenwirkungen), werden Sie sich nicht darum kümmern, oder?
Eugene
2
@Titulum Java 8-Funktionen haben Java nicht in eine funktionale Sprache verwandelt, und Streams usw. sind kein Ersatz , sondern ein zusätzliches Tool. Das Speichern von Dingen in einer Datenbank ist ein großer Nebeneffekt. Sie versuchen also, etwas zu beschneiden, das nicht passt, nur weil Sie die Idee der funktionalen Programmierung mögen. Vielleicht möchten Sie sich Clojure ansehen, wenn Sie alle Funktionen nutzen möchten.
Kayaman
1
FWIW, was diesen Nebeneffekt noch schlimmer machen kann, ist, dass es tatsächlich workauf alten Java 8-Versionen, aber nicht auf Java 9 oder höher wäre. Diese spezielle Optimierung für Streams mit Größe wurde
Stefan Zobel eingeführt