私はこれに対する答えを知っていると思いますが、欠けている穴や欠けているものを探しています。
これはSpringとJava=に焦点を当てていますが、実際にはどのプログラミングスタックにも適用できます。
とにかく、Springでは@Transactional
で注釈された典型的なサービスレイヤーがあります。たとえば、次のようにすることができます。
EmailService
OrderService
HistoryService
これで、ユーザーはRESTful
サービスを呼び出すアクションを実行して、次のことを行います。
orderService.create(....); // wrapped in @Transactional
historyService.create(....); // wrapped in @Transactional
emailService.emailUser(....); // wrapped in @Transactional (also annotated with @Async)
これらはすべてコントローラー(@Transactionalではありません)から呼び出されますが、注文サービスまたは履歴サービスのいずれかが失敗した場合、両方をロールバックして、メールサービスを中止します。
サービスを混在させるのは好きではありません。両方が同じトランザクション境界内にあるように、注文サービスが履歴サービスを呼び出すのは醜いと思います。
私の最初の本能は、両方のサービスを一緒にラップするハイブリッドサービスを作成することでした。何かのようなもの:
@Transactional
public void orderEntryService.create(....) {
orderService.create(....); // STILL wrapped in @Transactional
historyService.create(....); // STILL wrapped in @Transactional
}
このように、私のコントローラーは次のようになります:
public String create(...) {
orderEntryService.create(...);
emailService.emailUser(...); // this is @Async
// and will never be called if previous
// orderEntryService.create fails
}
これによりサービスレイヤーがよりクリーンに保たれると思いますが、かさばって忘れっぽい「集約」サービスクラスがすぐに追加され始める可能性があります。これをどのように処理しますか?
このようなサービスミックスインも私には自然に見えないので、私はあなたの疑問を理解しています。しかし、なぜ?厳密に言えば、完全であってもSOAアーキテクチャサービスの構成は禁止されていません。ただし、ケースは異なります。サービスは、独立したトランザクション管理を備えた独自のプロセスにある独立したユニットではありません。サービスは、シングルプロセスアーキテクチャに加えて、サービスのメソッドはデータベーストランザクションにラップされています。そのため、サービス構成はネストされたトランザクションにつながる可能性があり、それ自体はSpringの問題ではありませんが、それでも私には不自然に見えます。さらに、このような構成は、 循環依存関係サービス間で、間違いなく悪です。
リポジトリにEmailRepository、OrderRepository、HistoryRepositoryが含まれるように、アーキテクチャにもう1つのレベル(DataAccessとしましょう)を追加することで問題を解決できます。ここには、電子メール、注文、履歴などを管理するためのメソッドがあります:追加、更新、削除、クエリ。そして、これらのリポジトリをさまざまなサービスで共有し、サービスの周囲に現在の場所にトランザクションラッパーを置くことができます。
これは、DDDなどのより洗練されたアプローチについて説明するのではなく、役立つ可能性がある最も単純なソリューションです。
明確な答えはありませんが、私のポイントは次のとおりです。
賛成:
コントローラのテストは難しい。サービスは簡単です。 WebApplicationContext、json de/serialization、コントローラーのエラー処理、SpringSecurityFilterChainなどのモックを回避します。承認をモックする必要がある場合もあります。もちろん、とにかくコントローラーをテストしようとしますが、これらはサーバーとクライアント間の通信に焦点を当てたより重いテストです。ロケールと入力のサンプル化では、十分に複雑です。
トランザクションで実行する必要があるタスク。 shipGoodsとchargeCreditCardの2つのサービスを実行する必要があるとしましょう。 shipGoodsが正常に実行され、その後chargeCreditCardが失敗します。発送注文を元に戻したいと考えています。別の方法:サービスで完全なビジネスロジックのみが呼び出される場合、DAOとサービスの間に「コンポーネント」レベルを作成できます。そして、transactionall DAOは必要ありません。
大きなトランザクションが必要ない場合は、小さなトランザクションを許可します。 opossiteのケースでは、検索サービスやWebサービスの呼び出しなど、多くの機能を呼び出している可能性があります。トランザクションをロールバックする必要がない場合は、できるだけ早くコミットすることをお勧めします。開いているトランザクションは、データベースレコード(またはテーブル全体)をロックして、パフォーマンスを低下させる可能性があります。例:データベースのエンティティを更新し、遅いリモートWebサービスを呼び出して別のエンティティを作成します。大きな変換アプローチでは、エンティティは最後の作成が終了するまでコミットされず、元のレコードを更新することはできません。
コードの再利用:サービスが別のサービスを呼び出すことなくコードを再利用することは困難です。非常にテストされた「chargeCreditCard」メソッドがあるとします。 「shipGoods」が「preorderItem」に変更された場合でも再利用したいかもしれません。メールをサービスに送信することを検討している場合(emailServiceで判断すると思われる場合)、「forgottenPasswordService」、「newsLetterService」、「shippingService」は、機能を再利用することで実際にメリットを得られるようです。
に対して: