我说事务不属于模型层的原因基本上是这样的:
模型可以调用其他模型中的方法。
如果模型尝试启动事务,但它不知道其调用者是否已经启动事务,则模型必须有条件地启动事务,如@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.