Общий откат вложенных транзакций

Мне действительно нравится это решение NestedPDO для Yii, но мне нужна другая обработка транзакций.

Я хочу зафиксировать свои вложенные транзакции, только если все вложенные транзакции могут быть зафиксированы, и если ОДНА транзакция выполняет откат, все транзакции должны быть откатаны.

Как я могу это сделать?

Моя попытка изменить функцию rollBack, которая не сработала:

public function rollBack() {
    $this->transLevel--;

    if($this->transLevel == 0 || !$this->nestable()) {
        parent::rollBack();
    } else {

        $level = $this->transLevel;
        for($level; $level>1; $level--){
            $this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->constantlevel}");
        }
        //parent::rollBack();
    }
}

Я думал об адаптации NestedPDO: в функции commit() выполняется фиксация только для самой внешней транзакции, а в функции rollBack() выполняется откат к самой внешней транзакции, независимо от того, какая суб-транзакция вызвала откат. Но я не мог сделать это...

Я использую таблицы MySQL и InnoDB, и я не уверен насчет автоматической фиксации, но при отражении значения автоматической фиксации в транзакции я всегда получаю значение 1, которое должно означать, что автоматическая фиксация включена, но в транзакции autocommit должен быть установлен на 0. I я не уверен, является ли это причиной того, что весь откат не работает для меня?

1 ответ

Если вы хотите, чтобы вся транзакция автоматически откатывалась при возникновении ошибки, вы можете просто повторно сгенерировать исключение из B обработчик исключений при вызове из некоторых определенных мест (например, из A() ):

function A(){
   ...
   $this->B(true);
   ...
}

/*
* @param B boolean Throw an exception if the transaction is rolled back
*/
function B($rethrow) {
    $transaction=Yii::app()->db->beginTransaction();
    try {
        //do something
        $transaction->commit();
    } catch(Exception $e) {
        $transaction->rollBack();
        if ($rethrow) throw $e;
    }
}

Теперь я понимаю, что вы на самом деле просто хотите, чтобы ваша оболочка обнаружила, выполняется ли уже транзакция, и в этом случае не запускает транзакцию.

Поэтому вам не нужно NestedPDO учебный класс. Вместо этого вы можете создать такой класс:

class SingleTransactionManager extends PDO {
    private $nestingDepth = 0;

    public function beginTransaction() {
        if(!$this->nestingDepth++ == 0) {
            parent::beginTransaction();
        } // else do nothing
    }
    public function commit() {
        $this->nestingDepth--;
        if (--$this->nestingDepth == 0) {
            parent::commit();
        } // else do nothing
    }

    public function rollback() {
        parent::rollback();
        if (--$this->nestingDepth > 0) {
            $this->nestingDepth = 0;
            throw new Exception(); // so as to interrupt outer the transaction ASAP, which has become pointless
        }

    }
}

Основываясь на ответе @RandomSeed, я создал "выпадение" для обработки транзакций Yii по умолчанию:

$connection = Yii::app()->db;
$transaction=$connection->beginTransaction();
try
{
   $connection->createCommand($sql1)->execute();
   $connection->createCommand($sql2)->execute();
   //.... other SQL executions
   $transaction->commit();
}
catch(Exception $e)
{
   $transaction->rollback();
}

Это мой класс SingleTransactionManager:

class SingleTransactionManager extends CComponent 
{
    // The current transaction level.
    private $transLevel = 0;

    // The CDbConnection object that should be wrapped
    public $dbConnection;

    public function init()
    {
        if($this->dbConnection===null)
            throw new Exception('Property `dbConnection` must be set.');

        $this->dbConnection=$this->evaluateExpression($this->dbConnection);
    }
    // We only start a transaction if we're the first doing so
    public function beginTransaction() {
        if($this->transLevel == 0) {
            $transaction = parent::beginTransaction();
        } else {
            $transaction = new SingleTransactionManager_Transaction($this->dbConnection, false);
        }
        // always increase transaction level:
        $this->transLevel++;

        return $transaction;
    }

    public function __call($name, $parameters)
    {
        return call_user_func_array(array($this->dbConnection, $name), $parameters);
    }
}

class SingleTransactionManager_Transaction extends CDbTransaction
{
    // boolean, whether this instance 'really' started the transaction
    private $_startedTransaction;

    public function __construct(CDbConnection $connection, $startedTransaction = false)
    {
        $this->_startedTransaction = $startedTransaction;
        parent::__construct($connection);
        $this->setActive($startedTransaction);
    }

    // We only commit a transaction if we've started the transaction
    public function commit() {
        if($this->_startedTransaction)
            parent::commit();
    }

    // We only rollback a transaction if we've started the transaction
    // else throw an Exception to revert parent transactions/take adquate action
    public function rollback() {
        if($this->_startedTransaction)
            parent::rollback();
        else
            throw new Exception('Child transaction rolled back!');
    }
}

Этот класс "оборачивает" основное соединение с базой данных, вы должны объявить его как компонент в вашей конфигурации:

'components'=>array(

    // database
    'db'=>array(
        'class' => 'CDbConnection',
        // using mysql
        'connectionString'=>'....',
        'username'=>'...',
        'password'=>'....',
    ),

    // database
    'singleTransaction'=>array(
        'class' => 'pathToComponents.db.SingleTransactionManager',
        'dbConnection' => 'Yii::app()->db'
    )

Обратите внимание, что dbConnection свойство должно быть выражением для подключения к основной базе данных. Теперь, когда вложенные транзакции во вложенных блоках try catch, вы можете создать ошибку, например, во вложенной транзакции 3, а транзакции на 1 и 2 также будут откатаны.

Тестовый код:

$connection = Yii::app()->singleTransaction;

$connection->createCommand('CREATE TABLE IF NOT EXISTS `test_transactions` (
  `number` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;')->execute();

$connection->createCommand('TRUNCATE TABLE `test_transactions`;')->execute();

testNesting(4, 3, 1);

echo '<br>';
echo 'Rows:';
echo '<br>';
$rows = $connection->createCommand('SELECT * FROM `test_transactions`')->queryAll();
if($rows)
{
    foreach($rows as $row)
    {
        print_r($row);
    }
}
else
    echo 'Table is empty!';

function testNesting(int $total, int $createErrorIn = null, int $current = 1)
{
    if($current>=$total)
        return;

    $connection = Yii::app()->singleTransaction;
    $indent = str_repeat('&nbsp;', ($current*4));

    echo $indent.'Transaction '.$current;
    echo '<br>';
    $transaction=$connection->beginTransaction();
    try
    {
        // create nonexisting columnname when we need to create an error in this nested transaction
        $columnname = 'number'.($createErrorIn===$current ? 'rr' : '');
        $connection->createCommand('INSERT INTO `test_transactions` (`'.$columnname.'`) VALUES ('.$current.')')->execute();

        testNesting($total, $createErrorIn, ($current+1));

        $transaction->commit();
    }
    catch(Exception $e)
    {
        echo $indent.'Exception';
        echo '<br>';
        echo $indent.$e->getMessage();
        echo '<br>';
        $transaction->rollback();
    }
}

Результаты в следующем выводе:

    Transaction 1
        Transaction 2
            Transaction 3
            Exception
            CDbCommand failed to execute the SQL statement: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'numberrr' in 'field list'. The SQL statement executed was: INSERT INTO `test_transactions` (`numberrr`) VALUES (3)
        Exception
        Child transaction rolled back!
    Exception
    Child transaction rolled back!

Rows:
Table is empty! 

ИМХО, идея имитации "вложенных транзакций" в коде приложения является анти-паттерном. Существует множество случаев аномалий, которые невозможно решить в приложении (см. Мой ответ на /questions/47552219/kak-obnaruzhit-chto-tranzaktsiya-uzhe-nachalas/47552271#47552271).

В PHP лучше быть проще. Работа естественно организована в запросы, поэтому используйте запрос как область транзакции.

  • Запустите транзакцию на уровне контроллера, прежде чем вызывать какие-либо классы моделей.
  • Пусть модели выдают исключения, если что-то пойдет не так.
  • Перехватите исключение на уровне контроллера и выполните откат при необходимости.
  • Зафиксируйте, если исключение не обнаружено

Забудьте обо всех глупостях об уровнях транзакций. Модели не должны запускать, совершать или откатывать какие-либо транзакции.

Другие вопросы по тегам