【问题标题】:Does Zend_Db_Adapter::beginTransaction() stack?Zend_Db_Adapter::beginTransaction() 是否堆栈?
【发布时间】:2011-06-23 05:32:18
【问题描述】:

考虑以下几点:

/** (Cas_Template_Tree::DeleteNode)
 * Deletes the given node from the tree, and all of it's children.
 *
 * @static
 * @throws Exception
 * @param Cas_Template_Node $node
 * @return void
 */
public static function DeleteNode(Cas_Template_Node $node)
{
    $table = new Cas_Table_Templates();
    $adapter = $table->getAdapter();
    $leftStr = $adapter->quoteIdentifier('Left');
    $rightStr = $adapter->quoteIdentifier('Right');
    try
    {
        $adapter->beginTransaction();
        $row = $table->find($node->GetId())->current();
        $dependantRowSelector = array(
            "$leftStr >= ?" => $row->Left,
            "$rightStr <= ?" => $row->Right
        );
        //Get the rows removed so that we can nuke the ACLs later.
        $rowsToDelete = $table->fetchAll($dependantRowSelector)->toArray();
        //Delete the rows.
        $table->delete($dependantRowSelector);
        //Delete access control lists on those rows.
        foreach ($rowsToDelete as $rowToDelete)
        {
            Cas_Acl::CreateExisting($rowToDelete['Acl'])->Delete();
        }
        $left = (int)$row->Left;
        $right = (int)$row->Right;
        $difference = $right - $left + 1;
        $table->update(array('Left' => new Zend_Db_Expr("$leftStr - $difference")),
                       array("$leftStr > ?" => $right));
        $table->update(array('Right' => new Zend_Db_Expr("$rightStr - $difference")),
                       array("$rightStr > ?" => $right));
        $adapter->commit();
    }
    catch (Exception $ex)
    {
        $adapter->rollBack();
        throw $ex;
    }
}

/** (Cas_Acl::Delete)
 * Removes this ACL (and all of its dependent access control entries) from the database.
 * @return void
 */
public function Delete()
{
    $aclTable = new Cas_Table_AccessControlLists();
    $aceTable = new Cas_Table_AccessControlEntries();
    $adapter = Zend_Db_Table_Abstract::getDefaultAdapter();
    $identifierName = $adapter->quoteIdentifier('Identifier');
    $aclName = $adapter->quoteIdentifier('Acl');
    try
    {
        $adapter->beginTransaction();
        $aceTable->delete(array("$aclName = ?" => $this->GetId()));
        $aclTable->delete(array("$identifierName = ?" => $this->GetId()));
    }
    catch (Exception $ex)
    {
        $adapter->rollBack();
        throw $ex;
    }
}

注意这两个都要求事务如何工作,否则操作将不是原子的(这会很糟糕;))但是,这里有两个事务块。原始的 DeleteNode 方法调用Cas_Acl::Delete(),它也尝试在事务块内执行自身。理想情况下,Zend_Db 会足够聪明地识别这种情况,并且对于这个特定的调用忽略 Cas_Acl::Delete 中的开始事务和提交/回滚调用。

上面的代码安全吗?能否以任何方式显着改善?

【问题讨论】:

  • 你有时会直接打电话给Delete()吗?还是只在DeleteNode() 内调用?
  • @singles:是的——只要需要删除 ACL,就会调用 Cas_Acl::Delete。该应用程序管理的大多数对象都附加了一个 ACL。其中一些对象使用事务,而另一些则不需要。
  • 你的代码提出了一个问题,就像黑线鳕的胡须在 tintin omg.wthax.org/haddock.jpg 的床单上或下,我认为“$adapter->beginTransaction();”应该设置在“try {} catch”之前而不是里面

标签: php zend-framework


【解决方案1】:

AFAIK Zend_Db 无法识别嵌套事务。看代码。

public function beginTransaction()
{
    $this->_connect();
    $q = $this->_profiler->queryStart('begin', Zend_Db_Profiler::TRANSACTION);
    $this->_beginTransaction();
    $this->_profiler->queryEnd($q);
    return $this;
}

这里没有代码可以识别另一个事务(但也许分析器可以用来做这样的事情)并且_beginTransaction 依赖于 PDO 的beginTransaction

您可能要做的是将第二个参数添加到Delete() 方法,该方法确定是否使用事务并在DeleteNode() 中使用false 参数调用它:

//RB called as Cas_Acl::CreateExisting($rowToDelete['Acl'])->Delete(false);
public function Delete($useTransaction = true)
{
    $aclTable = new Cas_Table_AccessControlLists();
    $aceTable = new Cas_Table_AccessControlEntries();
    $adapter = Zend_Db_Table_Abstract::getDefaultAdapter();
    $identifierName = $adapter->quoteIdentifier('Identifier');
    $aclName = $adapter->quoteIdentifier('Acl');
    try
    {
        if ($useTransaction === true) {
            $adapter->beginTransaction();
        }
        $aceTable->delete(array("$aclName = ?" => $this->GetId()));
        $aclTable->delete(array("$identifierName = ?" => $this->GetId()));
        //BTW isn't commit() should be called here? 
    }
    catch (Exception $ex)
    {   
        if ($useTransaction === true) {
            $adapter->rollBack();
        }
        throw $ex;
    }
}

【讨论】:

  • 这里可能会使用策略对象而不是布尔值(多态 > 条件)。否则很好的答案(+1)
【解决方案2】:

我使用自定义 transactionManager 对象并在此处添加此逻辑(如果尚未开始,则开始事务)。

我发现这种类型的对象(委托?)非常有用。我也使用它来使用具有正确访问权限的特定数据库连接。事务中的所有写入和读取查询都使用此 TransactionManager 对象。

对 roolback 的调用是在控制器中完成的,但也通过 TransactionManager 来检测它是否真的应该这样做,这也非常有用,因为调用 Rollback 两次会使大多数现有数据库崩溃。

最后我们尝试使用通用规则,在代码中使事务控制代码非常“高级”(仅在控制器上)而不是在更抽象的子级别对象中。因此,通常我们可以对大多数对象调用任何 ->Delete() 操作,并且我们知道该对象不会尝试处理事务,我们应该这样做。但是,实际上,这只是一般规则,有时会发生封闭事务,TransactionManager 帮助我们隐藏问题(并且可以使用堆栈跟踪记录它们)。

【讨论】:

  • +1 - 我可能会将所有事务逻辑移动到控制器中。 :( 我不喜欢“事务管理器”的想法 1. 因为 PHP 没有确定性破坏(因此 RAII 是不可能的),以及 2. 因为任何包含“管理器”一词的类都是代码味道。
  • @Billy ONeal:您有 register_shutdown 功能,无论如何,如果您不使用持久连接,事务将在出现问题时超时,因此确定性销毁不是问题。如果需要,您可以将其称为 TransactionAdapter,因此名称中没有管理或处理,而是真正的模式。
  • @regilero:与其说是名字,不如说是类的实际作用。像“经理”这样的词是“很好的填充词,因为我们不知道这个类应该做什么”——通常表示违反了 SRP。 (免责声明:我在这里的玻璃房子里——上面的第一种方法是在Cas_Template_Locator,这同样糟糕)。
  • @Billy ONeal:是的,关于违反 SRP 的说法是正确的,但是在这里,谈到事务责任,好的设计至少是从 DAO 中排除事务处理,并将事务责任放在单个对象中它负责事务处理(处理事务代码周围的 try/catch 和重试循环 - 因为好的事务有时会被 tiemouts 和死锁中断),一个复杂的代码,最好只写一次。
  • @regilero:抱歉——我的反应是下意识的反应——过多地查看学生的代码并看到他们在 everything 中添加了“经理”一词: (我同意创建一个单独的对象是有意义的。也许“RefCountedTransaction”或“TransactionSerializer”更有意义。
猜你喜欢
  • 2011-08-15
  • 2021-08-06
  • 2020-07-08
  • 2020-12-19
  • 2016-12-21
  • 2016-10-11
  • 1970-01-01
  • 2021-03-15
  • 1970-01-01
相关资源
最近更新 更多