22

我正在使用 Yii 框架开发一个 PHP/MySQL 应用程序。

我遇到过以下情况:

在我的VideoController中,我有一个actionCreate创建新视频并actionPrivacy 设置视频隐私的方法。问题在于,在调用当前具有事务的模型actionCreate方法setPrivacy期间。Video我希望视频的创建也可以在事务中进行,这会导致错误,因为事务已经处于活动状态。

在对此答案的评论中,比尔卡尔文写道

所以没有必要让域模型类或 DAO 类管理事务——只需在控制器级别进行即可

这个答案中:

由于您使用的是 PHP,因此您的事务范围至多是一个请求。所以你应该只使用容器管理的事务,而不是服务层事务。也就是说,在开始处理请求时启动事务,并在处理完请求时提交(或回滚)。

如果我在控制器中管理事务,我会有一堆代码,如下所示:

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 中,它可以是静态方法,也可以是简单的非面向对象代码。这变得非常混乱,并导致模型中出现大量重复代码。

这也是控制耦合的一个例子,这被认为是不好的,因为调用者必须了解被调用对象的内部工作原理。例如,您的模型的某些方法可能具有 $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 等一些框架中实现)是在 DBAL 知道它已经在事务中时进行beginTransaction()commit()无操作。但是,如果您的模型尝试提交并发现它并没有真正提交,这可能会导致异常。或者尝试回滚并忽略该请求。我以前写过关于这些异常的文章。

我建议的折衷方案是Models 不知道 transactions。模型不知道它的请求setPrivacy()是否应该立即提交,或者它是否是更大图景的一部分,涉及多个模型的更复杂的一系列更改,并且只有在所有这些更改都成功 时才应该提交。这就是交易的重点。

因此,如果模型不知道他们是否可以或应该开始并提交自己的事务,那么谁知道呢?GRASP 包括一个控制器模式,它是一个用例的非 UI 类,它被分配了创建和控制所有部分以完成该用例的责任。 控制器了解事务,因为这是所有关于完整用例是否复杂的信息都可以访问的地方,并且需要在模型中、在一个事务中(或者可能在多个事务中)进行多次更改。

我之前写过的例子,就是在beforeAction()一个MVC Controller的方法中启动一个事务,然后在方法中提交afterAction(),就是一个简化。Controller 应该可以自由地启动和提交逻辑上需要的尽可能多的事务以完成当前操作。或者有时控制器可以避免显式事务控制,并允许模型自动提交每个更改。

但关键是关于哪些交易是必要的信息是模型不知道的——必须告诉他们(以 $transactional 参数的形式)或者从他们的调用者那里查询它,这无论如何,都必须将问题一直委托给控制器的行动。

You may also create a Service Layer of classes that each know how to execute such complex use cases, and whether to enclose all the changes in a single transaction. That way you avoid a lot of repeated code. But it's not common for PHP apps to include a distinct Service Layer; the Controller's action is usually coincident with a Service Layer.

于 2013-03-29T16:49:05.650 回答
7

最佳实践: 将事务放在模型中,不要将事务放在控制器中。

MVC 设计模式的主要优点是:MVC 使模型类无需修改即可重用。使维护和实施新功能变得容易。

例如,假设您主要为浏览器开发,其中用户一次输入一个数据集合,并且您将数据操作移动到控制器中。后来你意识到你需要支持允许用户上传大量的数据集合,以便从命令行导入到服务器上。

如果所有数据操作都在模型中,您可以简单地吞入数据并将其传递给模型进行处理。如果控制器中有必要的(事务性)功能,则必须在 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();
    }
}

最后,您的直觉是正确的(回复:这会导致我在很多地方需要重复的代码来执行操作。)。构建您的模型以支持您拥有的无数事务需求,并让控制器仅确定它将在自己的上下文中使用哪个入口点(方法)。

于 2013-03-28T18:00:53.777 回答
3

不,你是对的。事务由控制器应该做的“创建”方法委托。您建议使用像 beforeAction() 这样的“包装器”是可行的方法。只需让控制器扩展或实现此类。看起来您正在寻找观察者类型模式或类似工厂的实现。

于 2013-03-25T02:09:12.623 回答
0

好吧,这些广泛事务(在整个请求上)的一个缺点是您限制了数据库引擎的并发能力,并且还增加了死锁的可能性。从这个角度来看,将事务只放在需要它们的地方并让它们只覆盖需要覆盖的代码可能会有所回报。

如果可能的话,我肯定会在模型中放置交易。可以通过在该模型中引入 BaseModel(所有模型的祖先)和变量 transactionLock 来解决重叠事务的问题。然后,您只需将开始/提交事务指令包装到尊重此变量的 BaseModel 方法中。

于 2013-03-25T02:24:29.707 回答