ストリームパイプラインでのデータベースへの保存


8

オラクルのウェブサイトのドキュメントによると:

ストリーム操作に対する動作パラメータの副作用は、一般に推奨されません。これらは、無意識の要件の意図しない違反や、その他のスレッドセーフティの危険につながることが多いためです。

これには、ストリームの要素をデータベースに保存することも含まれますか?

次の(疑似)コードを想像してみてください。

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());
}

この実装に対抗する望ましくない影響は何ですか?

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.
}

1
はい、これは悪いことであり、特定の条件下では苦痛になります。
ユージーン

どうして?これを通常のforループとして書くことの違いは何ですか?
Titulum

これは明らかですが、使用parallelStreamするとトランザクションコンテキストが確実に失われます。
グレイン、

このコードの設計に関する疑問-データベースに書き込むメソッドがなぜモデルを返し、更新したのですか?それは分離できませんか?データベースオブジェクトをあるフェーズで他のオブジェクトにマッピングし、別のフェーズでデータベースに書き込むことを意味します。
ナマン

4
ドキュメントには、副作用は「一般に、推奨されない」と記載されています。次に、「この特定の例についてはどうですか」と質問しますが、特定の例の問題を指摘する応答を受け取ったら、「これは単なる例です」と言います。それで、あなたの質問がこの特定の例についてではない場合、あなたの実際の質問は何ですか?公式のドキュメントで、仮説のユースケースごとに、一般的な声明がすでに出されているときに、声明が出されることを本当に期待していますか。
ホルガー

回答:


4

オラクルのウェブサイト上のドキュメントに従って[...]

このリンクはJava 8向けです。Java9(2017年にリリースされた)以降のバージョンのドキュメントは、この点についてより明確になっているため、読むことをお勧めします。具体的には:

ストリームの実装では、結果の計算を最適化する際にかなりの自由度が許可されています。たとえば、ストリームの実装は、演算(またはステージ全体)をストリームパイプラインから自由に除外できます。したがって、計算の結果に影響を及ぼさないことが証明できれば、動作パラメータの呼び出しを除外できます。これは、特に指定されていない限り(端末の操作forEachやなどforEachOrdered、動作パラメータの副作用が常に実行されるとは限らず、信頼してはならないことを意味します。(そのような最適化の具体例については、count()操作に記載されているAPIノートを参照してください。詳細については、ストリームパッケージのドキュメントの「副作用」セクションを参照してください。)

出典:インターフェイスのJava 9のJavadocStream

また、引用したドキュメントの更新版:

副作用

ストリーム操作に対する動作パラメータの副作用は、一般に推奨されません。これらは、無意識の要件の意図しない違反や、その他のスレッドセーフティの危険につながることが多いためです。
動作パラメータに副作用がある場合、明示的に述べられていない限り、保証はありません。

  • 他のスレッドへのそれらの副作用の可視性;
  • 同じストリームパイプライン内の「同じ」要素に対する異なる操作は、同じスレッドで実行されます。そして
  • ストリームの実装は、計算の結果に影響を及ぼさないことが証明できる場合、ストリームパイプラインから操作(またはステージ全体)を自由に除外できるため、その動作パラメーターは常に呼び出されます。

副作用の順序は驚くかもしれません。パイプラインは、ストリームソースの出会いの順序と一致する結果を生成するように制約される場合であっても(例えば、IntStream.range(0,5).parallel().map(x -> x*2).toArray()製造しなければなりません[0, 2, 4, 6, 8])、マッパー関数が個々の要素に適用される順序、または特定の要素に対して実行される動作パラメータのスレッド。

副作用を排除することも驚くべきことかもしれません。端末操作forEachとを除いてforEachOrdered、ストリームの実装が計算の結果に影響を与えずに動作パラメータの実行を最適化できる場合、動作パラメータの副作用が常に実行されるとは限りません。(特定の例については、count操作に。)

ソース:パッケージのJava 9のJavadocjava.util.stream

すべての強調は私のものです。

ご覧のとおり、現在の公式ドキュメントでは、ストリーム操作で副作用を使用することにした場合に発生する可能性がある問題について詳しく説明しています。また、上の非常に明確であるforEachforEachOrdered副作用の実行が保証されているのみで、端末の操作を(公式の例が示すように、スレッドの安全性の問題はまだ適用され、あなたを気にし)ています。


それが言われていて、あなたの特定のコードに関して、そして言われたコードだけ:

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

上記のコードには、Streams関連の問題はありません。

  • この.map()ステップが実行されるのは.collect()(公式ドキュメントがの代わりに推奨する変更可能な縮小操作.forEach(list::add))が.map()の出力に依存しており、この(つまりsaveCar()の)出力が入力と異なるため、ストリームが「証明できない」ためです。その [eliding] 「それは計算の結果に影響を与えません
  • それはparallelStream()そうではないので、以前は存在しなかった同時実行性の問題を導入すべきではありません(もちろん、誰かが.parallel()後で追加した場合、問題が発生する可能性がありforます—内部計算のために新しいスレッドを起動してループを並列化することを決定した場合と同様です))。

これは、その例のコードがGood Code™であることを意味するものではありません。.stream.map(::someSideEffect()).collect()コレクション内のすべてのアイテムに対して副作用操作を実行する方法としてのシーケンスは、よりシンプル/ショート/エレガントに見えますか?そのfor対応物より、そしてそれは時々そうかもしれません。しかし、ユージーン、ホルガー、その他の何人かがあなたに言ったように、これに取り組むより良い方法があります。
簡単に考えると、アイテムの数が多い場合を除いて、aを起動Streamするか、シンプルを反復するコストはfor無視できません。アイテムが多い場合は、次のようになります。a)新しいDBアクセスを作成したくないそれぞれについて、APIの方が良いでしょう。およびb)処理のパフォーマンスを大幅に低下させたくないsaveAll(List items) アイテムを順番に並べるので、最終的に並列化を使用することになり、まったく新しい一連の問題が発生します。


1
ほら、これが私が探していた答えです。動作を確認するドキュメントへのリンクが付いた素晴らしい説明。
Titulum

7

最も簡単な例は次のとおりです。

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

この場合、java-9以降でmapは実行されません。あなたはそれを知るのにまったく必要ないのでcount

副作用があなたに多くの痛みを引き起こす他の複数のケースがあります。特定の条件下で。


1
count()まだ実行されると思いますが、ソースから結果を生成できる場合、実装は中間ステップをスキップする可能性があります(ただし、実装レベルのifsがたくさんあります)
ernest_k

2
@Titulumはまったく別の質問であり、実装に依存します。しかし、そうです、そのようなものはカスタムのようにターミナル操作で実装されるべきCollectorです。
ユージーン

2
@Titulumこれはどこにも文書化されません。これらは実装の詳細です。しかし、あなたが(あなたの副作用部分のように)ドキュメンテーションに従っているなら、あなたはそれらについて気にしないでしょうか?
ユージーン

2
@Titulum Java 8の機能はJavaを関数型言語に変換しませんでした。また、Streamsなどは代わりのものではなく、追加のツールです。物事をデータベースに保存することは大きな副作用であり、関数型プログラミングのアイデアが気に入ったからといって、適合しないものに足を踏み入れようとしています。すべての機能を使いたい場合は、Clojureを確認することをお勧めします。
カヤマン

1
FWIW、この副作用をさらに悪化させる可能性workがあるのは、Java 9以降ではなく古いJava 8バージョンで実際に発生することです。サイズ指定されたストリームに対するその特定の最適化は、JDK-8067969で
Stefan Zobel
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.