web-dev-qa-db-ja.com

Spring MVCコントローラーを@Transactionalにしないのはなぜですか?

このトピックに関する質問はすでにいくつかありますが、Spring MVCコントローラーTransactionalを作成してはならない理由を説明するための回答はありません。見る:

なぜ?

  • 乗り越えられない技術的な問題はありますか?
  • アーキテクチャ上の問題はありますか?
  • パフォーマンス/デッドロック/同時実行性の問題はありますか?
  • 時には複数の個別のトランザクションが必要ですか?はいの場合、ユースケースは何ですか? (サーバーへの呼び出しが完全に成功または完全に失敗する単純化された設計が好きです。非常に安定した動作であると思われます)

背景:私は数年前にチームで非常に大きなERP C#/ NHibernate/Spring.Netで実装されたソフトウェアに取り組みました。サーバーへのラウンドトリップはまさにそのように実装されました。トランザクションは、コントローラーロジックに入る前に開かれ、コントローラーを出た後にコミットまたはロールバックされました。トランザクションはフレームワークで管理されているため、誰も気にする必要はありませんでしたシンプルで、数人のアーキテクトだけがトランザクションの問題を気にする必要があり、残りのチームは機能を実装しました。

私の観点から、それは私が今まで見た中で最高のデザインです。 Spring MVCで同じデザインを再現しようとすると、レイジーロードとトランザクションの問題で悪夢に陥り、同じ答えが出るたびに、コントローラーをトランザクションにしないでください。

事前にご回答いただきありがとうございます!

58
jeromerg

[〜#〜] tldr [〜#〜]:これは、アプリケーションのサービス層のみがデータベース/ビジネストランザクションのスコープを識別するために必要なロジックを持っているためです。コントローラーおよび永続化レイヤーは、設計上、トランザクションの範囲を認識できない/すべきではありません。

コントローラーは@Transactional、しかし実際には、サービス層をトランザクションのみにすることが一般的な推奨事項です(永続層もトランザクションであってはなりません)。

この理由は技術的な実現可能性ではなく、懸念の分離です。コントローラーの役割は、パラメーター要求を取得し、1つ以上のサービスメソッドを呼び出して、結果を結合して応答をクライアントに送り返すことです。

そのため、コントローラーには、要求実行のコーディネーター機能と、ドメインデータをDTOなどのクライアントが使用できる形式に変換する機能があります。

ビジネスロジックはサービスレイヤーにあり、永続化レイヤーはデータベースとの間でデータをやり取りするだけです。

データベーストランザクションの範囲は、実際には技術的な概念と同じくらいビジネス上の概念です。口座振替では、他の口座にクレジットが入る場合などにのみ口座から引き落とすことができるため、ビジネスロジックを含むサービスレイヤーのみが本当に銀行口座振替取引の範囲。

永続層は、どのトランザクションに属しているかを知ることができません。たとえば、メソッドcustomerDao.saveAddress。常に独自の独立したトランザクションで実行する必要がありますか?知る方法はありません。それを呼び出すビジネスロジックに依存します。別のトランザクションで実行する必要がある場合もあれば、saveCustomerも機能した場合にのみデータを保存することもあります。

同じことがコントローラーにも当てはまります。saveCustomersaveErrorMessagesは同じトランザクションに入れるべきですか?顧客を保存し、それが失敗した場合は、データベースに保存するエラーメッセージを含むすべてをロールバックするのではなく、いくつかのエラーメッセージを保存してクライアントに適切なエラーメッセージを返そうとします。

非トランザクションコントローラーでは、サービスレイヤーから戻るメソッドは、セッションが閉じられているため、切り離されたエンティティを返します。これは正常です。解決策は、OpenSessionInViewを使用するか、コントローラーが必要と認識している結果を積極的にフェッチするクエリを実行することです。

とはいえ、コントローラーをトランザクション対応にすることは犯罪ではなく、最も頻繁に使用されるプラクティスではありません。

97

中規模から大規模のビジネスWebアプリケーションで、さまざまなWebフレームワーク(JSP/Struts 1.x、GWT、JSF 2、Java EEおよびSpring )。

私の経験では、最高レベル、つまり「コントローラー」レベルでトランザクションを区分することが最善です。

1つのケースでは、StrutsのBaseActionクラスを拡張するActionクラスがあり、Hibernateセッション管理を処理するexecute(...)メソッドの実装(ThreadLocal object)、トランザクション開始/コミット/ロールバック、および例外のユーザーフレンドリーなエラーメッセージへのマッピング。このメソッドは、例外がこのレベルまで伝播された場合、またはロールバック専用としてマークされた場合、現在のトランザクションを単にロールバックします。そうでなければ、トランザクションをコミットします。これはすべてのケースで機能し、通常、HTTPリクエスト/レスポンスサイクル全体で単一のデータベーストランザクションが存在します。複数のトランザクションが必要なまれなケースは、ユースケース固有のコードで処理されます。

GWT-RPCの場合、同様のソリューションが基本GWTサーブレット実装によって実装されました。

JSF 2では、これまでサービスレベルの境界設定のみを使用しました(自動的に「必要な」トランザクション伝播を持つEJBセッションBeanを使用)。ここには、JSFバッキングBeanのレベルでトランザクションを区別するのとは対照的に、欠点があります。基本的に、問題は多くの場合、JSFコントローラーが複数のサービス呼び出しを行う必要があり、各呼び出しがアプリケーションデータベースにアクセスすることです。サービスレベルのトランザクションでは、これはいくつかの別個のトランザクション(例外が発生しない限りすべてコミットされる)を意味し、データベースサーバーにより多くの負荷がかかります。ただし、これはパフォーマンス上の欠点ではありません。単一の要求/応答に対して複数のトランザクションがあると、微妙なバグにつながる可能性があります(詳細はもう覚えていませんが、そのような問題が発生しただけです)。

この質問に対する他の回答は、「データベース/ビジネストランザクションの範囲を識別するために必要なロジック」について語っています。通常、トランザクションの境界設定に関連付けられたnoロジックがあるため、この引数は意味がありません。コントローラクラスもサービスクラスも、トランザクションを実際に「知る」必要はありません。ほとんどの場合、Webアプリでは、各ビジネスオペレーションはHTTPリクエスト/レスポンスペア内で発生し、トランザクションの範囲は、リクエストが受信されてからレスポンスが完了するまで実行される個々のすべてのオペレーションです。

場合によっては、ビジネスサービスまたはコントローラーが特定の方法で例外を処理し、おそらく現在のトランザクションをロールバックのみにマークする必要があります。 Java EE(JTA)では、これは serTransaction#setRollbackOnly() を呼び出すことで行われます。UserTransactionオブジェクトは_@Resource_フィールド、またはいくつかのThreadLocalからプログラムで取得されます。Springでは、_@Transactional_アノテーションにより、特定の例外タイプに対してロールバックを指定できます。または、コードはスレッドローカルを取得できます TransactionStatus そしてsetRollbackOnly()を呼び出します。

したがって、私の意見と経験では、コントローラーをトランザクション対応にすることがより良いアプローチです。

17
Rogério

例外がスローされたときにトランザクションをロールバックしたい場合もありますが、同時に例外を処理したい場合は、コントローラーに適切な応答を作成します。

@Transactionalコントローラーメソッドで、ロールバックを強制してコントローラーメソッドからトランザクションをスローする唯一の方法ですが、通常の応答オブジェクトを返すことはできません。

更新:ロデリオの答え に概説されているように、プログラムでロールバックを実現することもできます。

より良い解決策は、サービスメソッドをトランザクション対応にしてから、コントローラーメソッドで発生する可能性のある例外を処理することです。

次の例は、createUserメソッドを使用したユーザーサービスを示しています。このメソッドは、ユーザーを作成し、ユーザーにメールを送信します。メールの送信に失敗した場合、ユーザー作成をロールバックします。

@Service
public class UserService {

    @Transactional
    public User createUser(Dto userDetails) {

        // 1. create user and persist to DB

        // 2. submit a confirmation mail
        //    -> might cause exception if mail server has an error

        // return the user
    }
}

次に、コントローラーでcreateUserへの呼び出しをtry/catchでラップし、ユーザーへの適切な応答を作成できます。

@Controller
public class UserController {

    @RequestMapping
    public UserResultDto createUser (UserDto userDto) {

        UserResultDto result = new UserResultDto();

        try {

            User user = userService.createUser(userDto);

            // built result from user

        } catch (Exception e) {
            // transaction has already been rolled back.

            result.message = "User could not be created " + 
                             "because mail server caused error";
        }

        return result;
    }
}

@Transactionコントローラーメソッドで、これは単に不可能です。

6
lanoxx