web-dev-qa-db-ja.com

シンプルおよびコンポジットトランザクションサービス:懸念とトランザクションの分離に関する質問

私はこれに対する答えを知っていると思いますが、欠けている穴や欠けているものを探しています。

これは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
}

これによりサービスレイヤーがよりクリーンに保たれると思いますが、かさばって忘れっぽい「集約」サービスクラスがすぐに追加され始める可能性があります。これをどのように処理しますか?

5
cbmeeks

このようなサービスミックスインも私には自然に見えないので、私はあなたの疑問を理解しています。しかし、なぜ?厳密に言えば、完全であってもSOAアーキテクチャサービスの構成は禁止されていません。ただし、ケースは異なります。サービスは、独立したトランザクション管理を備えた独自のプロセスにある独立したユニットではありません。サービスは、シングルプロセスアーキテクチャに加えて、サービスのメソッドはデータベーストランザクションにラップされています。そのため、サービス構成はネストされたトランザクションにつながる可能性があり、それ自体はSpringの問題ではありませんが、それでも私には不自然に見えます。さらに、このような構成は、 循環依存関係サービス間で、間違いなく悪です。

リポジトリにEmailRepository、OrderRepository、HistoryRepositoryが含まれるように、アーキテクチャにもう1つのレベル(DataAccessとしましょう)を追加することで問題を解決できます。ここには、電子メール、注文、履歴などを管理するためのメソッドがあります:追加、更新、削除、クエリ。そして、これらのリポジトリをさまざまなサービスで共有し、サービスの周囲に現在の場所にトランザクションラッパーを置くことができます。

これは、DDDなどのより洗練されたアプローチについて説明するのではなく、役立つ可能性がある最も単純なソリューションです。

2
ialekseev

明確な答えはありませんが、私のポイントは次のとおりです。

賛成:

  • コントローラーのビジネスロジックが好きではありません。彼らはすでに、ビュー、フォーマット、変換、バインディング、マッピングなどに関連する定型コードをたくさん持っています。独自のエラー処理を伴う3つの呼び出し(およびいくつかの特別なケース)は、バグをもたらすほど複雑になる可能性があります。注文を作成する方法のロジックは、注文の呼び出し方法とは別にするほど複雑になる場合があります。
  • コントローラのテストは難しい。サービスは簡単です。 WebApplicationContext、json de/serialization、コントローラーのエラー処理、SpringSecurityFilterChainなどのモックを回避します。承認をモックする必要がある場合もあります。もちろん、とにかくコントローラーをテストしようとしますが、これらはサーバーとクライアント間の通信に焦点を当てたより重いテストです。ロケールと入力のサンプル化では、十分に複雑です。

  • トランザクションで実行する必要があるタスクshipGoodschargeCreditCardの2つのサービスを実行する必要があるとしましょう。 shipGoodsが正常に実行され、その後chargeCreditCardが失敗します。発送注文を元に戻したいと考えています。別の方法:サービスで完全なビジネスロジックのみが呼び出される場合、DAOとサービスの間に「コンポーネント」レベルを作成できます。そして、transactionall DAOは必要ありません。

  • 大きなトランザクションが必要ない場合は、小さなトランザクションを許可します。 opossiteのケースでは、検索サービスやWebサービスの呼び出しなど、多くの機能を呼び出している可能性があります。トランザクションをロールバックする必要がない場合は、できるだけ早くコミットすることをお勧めします。開いているトランザクションは、データベースレコード(またはテーブル全体)をロックして、パフォーマンスを低下させる可能性があります。例:データベースのエンティティを更新し、遅いリモートWebサービスを呼び出して別のエンティティを作成します。大きな変換アプローチでは、エンティティは最後の作成が終了するまでコミットされず、元のレコードを更新することはできません。

  • コードの再利用:サービスが別のサービスを呼び出すことなくコードを再利用することは困難です。非常にテストされた「chargeCreditCard」メソッドがあるとします。 「shipGoods」が「preorderItem」に変更された場合でも再利用したいかもしれません。メールをサービスに送信することを検討している場合(emailServiceで判断すると思われる場合)、「forgottenPasswordService」、「newsLetterService」、「shippingService」は、機能を再利用することで実際にメリットを得られるようです。

に対して:

  • 複雑なトランザクション:3つのサービスを含む大きなトランザクションを作成する場合、それぞれが失敗したときに何が起こるかを考える必要があります。トランザクションサービスを呼び出す非トランザクションサービスの作成を禁止するものはありません。それは簡単なケースです。ネストされたトランザクションと自律型トランザクションは強力ですが、困難で危険な場合があります。
  • 複雑さ:サービスを呼び出すサービスを呼び出す多くのサービスで終了することができます。本当に複雑なビジネスロジックがありますか? CRUD操作のみでロジックはほとんどないトランザクションDAOなどのサービスの作成に注意してください。例:OrderServiceHistoryService。この場合、OrderServiceがhistoryDAOを呼び出し、注文を作成するときに自動的に履歴を含める必要があると考えることができます。 ( Anemic Domain Model Antipattern は避けてください)。
1
Borjab