开发者

Does Zend_Db_Adapter::beginTransaction() stack?

Consider the following:

/** (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;
    }
}

Notice how both of these require that transactions work, because otherwise the operation would not be atomic (which would be bad ;) ) However, there are two transaction blocks going on here. The original DeleteNode method calls Cas_Acl::Delete(), which also attempts to execute itself inside of a transaction block. Ideally, Zend_Db would be smart enough to recognize this case, and for this particular call ignore the begin transaction and commit/rollback calls inside Cas_Acl::Delete.

Would the above code be safe? Can it be significantly improved 开发者_StackOverflow社区in any way?


AFAIK Zend_Db is not able to recognize nested transactions. Look a the code.

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

There's no code here to recognize another transaction (but maybe profiler could be used to do such thing) and _beginTransaction relies on PDO's beginTransaction.

Thing that you might to do is add second param to Delete() method which determines whether to use transactions or not and call it with false param in DeleteNode():

//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;
    }
}


I use a custom transactionManager object and add this logic (start transaction if not yet started) there.

I found this type of object (Delegation?) quite useful. I use it as well to use specific DB connection with right access. All write and read query inside a transaction are using this TransactionManager object.

Call to roolback are done in the controller, but via the TransactionManager as well, which detects if it should really do it, and it is very useful as well because calling Rollback twice will make most of existing databases cry.

Finally we try to use a generic rule, make the transaction control code very "high-level" in the code (only on controllers) and not in more abstract sub-levels objects. So that generally we can call any ->Delete() action on most objecs and we know this object is not trying to handle the transaction and that WE should do it. But, effectively, this is only a general rule and sometimes enclosing transactions happen and the TransactionManagers help us to hide the problem (and can log them with stacktrace).

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜