22

私は、Yii フレームワークを使用して PHP/MySQL アプリに取り組んでいます。

次の状況に遭遇しました。

VideoControllerの にactionCreateは、新しいビデオを作成actionPrivacy し、ビデオのプライバシーを設定する があります。問題は、現在トランザクションを持っているモデルactionCreatesetPrivacyメソッドが呼び出されている間です。Videoビデオの作成もトランザクションに含めたいと思いますが、トランザクションが既にアクティブであるため、エラーが発生します。

この回答のコメントで、Bill Karwin は次のように書いています。

そのため、ドメイン モデル クラスや DAO クラスでトランザクションを管理する必要はありません。コントローラー レベルで行うだけです。

そしてこの答えで:

PHP を使用しているため、トランザクションのスコープは最大でも 1 つのリクエストです。したがって、サービス層のトランザクションではなく、コンテナー管理のトランザクションのみを使用する必要があります。つまり、リクエストの処理の開始時にトランザクションを開始し、リクエストの処理が終了したらコミット (またはロールバック) します。

コントローラーでトランザクションを管理すると、次のような一連のコードが作成されます。

public function actionCreate() {
  $trans = Yii::app()->getDb()->beginTransaction();
  ...action code...
  $trans->commit();
}

これにより、アクションにトランザクションが必要な多くの場所でコードが重複することになります。

または、親クラスのbeforeAction()andメソッドにリファクタリングして、実行されるアクションごとにトランザクションを自動的に作成することもできます。afterAction()Controller

この方法で何か問題はありますか?PHP アプリのトランザクション管理の良い方法は何ですか?

4

4 に答える 4

22

トランザクションがモデル層に属さないと私が言う理由は、基本的に次のとおりです。

モデルは、他のモデルのメソッドを呼び出すことができます。

モデルがトランザクションを開始しようとしたが、呼び出し元が既にトランザクションを開始したかどうかを認識していない場合、 @Bubba の回答のコード例に示すように、モデルは条件付きでトランザクションを開始する必要があります。モデルのメソッドは、呼び出し元が独自のトランザクションの開始が許可されているかどうかを伝えることができるように、フラグを受け入れる必要があります。または、モデルは呼び出し元の「トランザクション中」の状態を照会する機能を備えている必要があります。

public function setPrivacy($privacy, $caller){
    if (! $caller->isInTransaction() ) $this->beginTransaction();

    $this->privacy = $privacy;
    // ...action code..

    if (! $caller->isInTransaction() ) $this->commit();
}

呼び出し元がオブジェクトでない場合はどうなりますか? PHP では、静的メソッドまたは単に非オブジェクト指向のコードである可能性があります。これは非常に面倒になり、モデル内で多くのコードが繰り返されることになります。

これはControl Couplingの例でもあります。これは、呼び出し元が呼び出されたオブジェクトの内部動作について何かを知っている必要があるため、悪いと見なされます。たとえば、モデルの一部のメソッドには $transactional パラメーターがある場合がありますが、他のメソッドにはそのパラメーターがない場合があります。パラメータが重要な場合、呼び出し元はどのように知る必要がありますか?

// I need to override method's attempt to commit
$video->setPrivacy($privacy, false);  

// But I have no idea if this method might attempt to commit
$video->setFormat($format); 

私が提案した (または Propel のようないくつかのフレームワークで実装された) もう 1 つの解決策はbeginTransaction()commit()DBAL が既にトランザクション内にあることを認識している場合に、何もしないことです。ただし、モデルがコミットしようとして、実際にはコミットされていないことが判明した場合、これは異常につながる可能性があります。または、ロールバックを試みて、その要求を無視します。私は以前にこれらの異常について書きました。

私が提案した妥協点は、モデルがトランザクションについて知らないということです。モデルは、その要求がsetPrivacy()すぐにコミットする必要があるものなのか、それともより大きな全体像 (複数のモデルが関与するより複雑な一連の変更) の一部であり、これらすべての変更が成功した場合に のみコミットする必要があるのか​​を認識していません。それが取引のポイントです。

モデルが自分のトランザクションを開始してコミットできるかどうか、またはコミットする必要があるかどうかがわからない場合、誰がそれを行うのでしょうか? GRASP には、ユース ケースの非 UI クラスであるコントローラー パターンが含まれており、そのユース ケースを達成するためにすべての部分を作成および制御する責任が割り当てられています。 コントローラーはトランザクションについて知っています。これは、完全なユースケースが複雑であるかどうか、モデルで複数の変更を 1 つのトランザクション内 (または複数のトランザクション内) で行う必要があるかどうかに関するすべての情報にアクセスできる場所であるためです。

以前に書いたbeforeAction()、MVC コントローラーのメソッドでトランザクションを開始し、メソッドでコミットするという例afterAction()単純化したものです。コントローラーは、現在のアクションを完了するために論理的に必要な数のトランザクションを自由に開始およびコミットできる必要があります。または、コントローラーが明示的なトランザクション制御を控え、モデルが各変更を自動コミットできるようにする場合もあります。

しかし重要なのは、どのトランザクションが必要であるかについての情報は、モデルが知らないものだということです。モデルは ($transactional パラメータの形式で) 伝えられるか、呼び出し元からクエリを実行する必要があります。とにかく、コントローラのアクションまで質問を委任する必要があります。

また、このような複雑なユースケースを実行する方法と、すべての変更を単一のトランザクションに含めるかどうかをそれぞれが知っているクラスのサービス層を作成することもできます。そうすれば、多くのコードの繰り返しを避けることができます。しかし、PHP アプリに個別のサービス レイヤーを含めることは一般的ではありません。通常、コントローラーのアクションはサービス層と一致します。

于 2013-03-29T16:49:05.650 に答える
7

ベスト プラクティス: トランザクションをモデルに配置し、トランザクションをコントローラーに配置しないでください。

MVC 設計パターンの主な利点は次のとおりです。MVC により、モデル クラスが変更なしで再利用可能になります。メンテナンスと新機能の実装を容易にします。

たとえば、主に、ユーザーが一度に 1 つのデータ コレクションを入力するブラウザー用に開発し、データ操作をコントローラーに移したとします。後で、ユーザーが多数のデータ コレクションをアップロードして、コマンド ラインからサーバーにインポートできるようにする必要があることに気付きました。

すべてのデータ操作がモデル内にある場合は、単純にデータを丸呑みしてモデルに渡して処理することができます。コントローラーに必要な (トランザクション) 機能がある場合は、CLI スクリプトでそれを複製する必要があります。

一方で、別のポイントから同じ機能を実行する必要がある別のコントローラーが必要になる場合があります。他のコントローラーでもコードを複製する必要があります。

そのためには、モデル内のトランザクションの課題を解決するだけで済みます。

すでにトランザクションが組み込まれている setPrivacy() メソッドを持つ Video クラス (モデル) があると仮定します。より大きなトランザクションでその機能をラップする必要がある別のメソッド persist() からそれを呼び出したい場合は、条件付きトランザクションを実行するように setPrivacy() を変更するだけで済みます。

おそらく、このようなものです。

class Video{
    private $privacy;
    private $transaction;

    public function __construct($privacy){

        $this->privacy = $privacy;
    }

    public function persist(){
        $this->beginTransaction();
        // ...action code...
        $this->setPrivacy($this->privacy, false);
        // ...action code...
        $this->commit();
    }

    public function setPrivacy($privacy, $transactional = true){
        if ($transactional) $this->beginTransaction();

        $this->privacy = $privacy;
        // ...action code..

        if ($transactional) $this->commit();
    }


    private function beginTransaction(){
        $this->transaction = Yii::app()->getDb()->beginTransaction();
    }

    private function commit(){
        $this->transaction->commit();
    }
}

最後に、あなたの本能は正しいです (re:アクションのためにトランザクションが必要な多くの場所でコードの重複につながります。 )。無数のトランザクション ニーズをサポートするようにモデルを設計し、コントローラーが独自のコンテキストで使用するエントリ ポイント (メソッド) を決定するだけにします。

于 2013-03-28T18:00:53.777 に答える
3

いいえ、あなたは正しいです。トランザクションは、コントローラーが行うべき「作成」メソッドによって委任されます。beforeAction() のような「ラッパー」を使用するというあなたの提案が道です。コントローラーにこのクラスを拡張または実装させるだけです。Observer 型のパターンまたはファクトリのような実装を探しているようです。

于 2013-03-25T02:09:12.623 に答える
0

これらの広範なトランザクション (リクエスト全体にわたる) の欠点の 1 つは、データベース エンジンの同時実行機能が制限され、デッドロックの可能性も高くなることです。この観点からすると、必要な場所にのみトランザクションを配置し、カバーする必要があるコードのみをカバーできるようにすることは効果的かもしれません。

可能であれば、私は間違いなくモデルにトランザクションを配置します. 重複するトランザクションの問題は、そのモデルに BaseModel (すべてのモデルの祖先) と変数 transactionLock を導入することで解決できます。次に、begin/commit トランザクション ディレクティブを、この変数を尊重する BaseModel メソッドにラップするだけです。

于 2013-03-25T02:24:29.707 に答える