在Controller中进行事务管理是不是很糟糕?

时间:2013-03-21 01:35:58

标签: php mysql design-patterns transactions

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

我遇到过以下情况:

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

this answer的评论中,Bill Karwin写道

  

因此无需管理域模型类或DAO类   交易 - 只需在控制器级别执行

并在this answer中:

  

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

如果我管理控制器中的事务,我会看到一堆代码如下:

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

这导致我需要事务处理的许多地方重复代码。

或者我可以将它重构为父beforeAction()类的afterAction()Controller方法,然后这些方法会自动为正在执行的每个操作创建事务。

这种方法有问题吗? PHP应用程序的事务管理有什么好的做法?

4 个答案:

答案 0 :(得分:18)

我说交易不属于模型层的原因基本上是这样的:

模型可以调用其他模型中的方法。

如果模型尝试启动事务,但它不知道其调用者是否已启动事务,则模型必须有条件地启动事务,如代码示例中所示@Bubba's answer。模型的方法必须接受一个标志,以便调用者可以告诉它是否允许它开始自己的事务。否则模型必须能够查询其调用者的“处于事务”状态。

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)是在DBAL知道它已经在事务中时使beginTransaction()commit()无操作。但是如果你的模型试图提交并发现它没有真正提交,这可能会导致异常。或尝试回滚并忽略该请求。我之前写过关于这些异常的文章。

我建议的妥协是模型不了解交易。该模型不知道它对setPrivacy()的请求是否应该立即提交,还是它是更大图片的一部分,更复杂的一系列更改涉及多个模型,并且如果所有这些变化都成功,我们将致这就是交易的重点。

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

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

但重点是关于什么样的交易是必要的信息是模型不知道的 - 他们必须被告知(以$ transactional参数的形式)或者从他们的查询中查询调用者,无论如何都必须将问题一直委托给Controller的行动。

您还可以创建一个Service Layer个类,每个类都知道如何执行这些复杂的用例,以及是否将所有更改包含在单个事务中。这样你就避免了很多重复的代码。但PHP应用程序包含不同的服务层并不常见; Controller的操作通常与服务层重合。

答案 1 :(得分: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();
    }
}

最后,你的直觉是正确的(re:这导致我需要事务处理的许多地方重复代码。)。构建您的模型以支持您拥有的大量事务需求,并让控制器仅确定它将在其自己的上下文中使用哪个入口点(方法)。

答案 2 :(得分:3)

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

答案 3 :(得分:0)

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

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

相关问题