mirror of
				https://github.com/yiisoft/yii2.git
				synced 2025-11-04 14:46:19 +08:00 
			
		
		
		
	Add transaction/retry support for yii\db\Command
This commit is contained in:
		@ -100,6 +100,16 @@ class Command extends Component
 | 
				
			|||||||
     * @var string name of the table, which schema, should be refreshed after command execution.
 | 
					     * @var string name of the table, which schema, should be refreshed after command execution.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    private $_refreshTableName;
 | 
					    private $_refreshTableName;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var string|false|null the isolation level to use for this transaction.
 | 
				
			||||||
 | 
					     * See [[Transaction::begin()]] for details.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private $_isolationLevel = false;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var callable a callable (e.g. anonymous function) that is called when [[\yii\db\Exception]] is thrown
 | 
				
			||||||
 | 
					     * when executing the command.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private $_retryHandler;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -991,7 +1001,7 @@ class Command extends Component
 | 
				
			|||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $profile and Yii::beginProfile($rawSql, __METHOD__);
 | 
					            $profile and Yii::beginProfile($rawSql, __METHOD__);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $this->pdoStatement->execute();
 | 
					            $this->internalExecute($rawSql);
 | 
				
			||||||
            $n = $this->pdoStatement->rowCount();
 | 
					            $n = $this->pdoStatement->rowCount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $profile and Yii::endProfile($rawSql, __METHOD__);
 | 
					            $profile and Yii::endProfile($rawSql, __METHOD__);
 | 
				
			||||||
@ -999,9 +1009,9 @@ class Command extends Component
 | 
				
			|||||||
            $this->refreshTableSchema();
 | 
					            $this->refreshTableSchema();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return $n;
 | 
					            return $n;
 | 
				
			||||||
        } catch (\Exception $e) {
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
            $profile and Yii::endProfile($rawSql, __METHOD__);
 | 
					            $profile and Yii::endProfile($rawSql, __METHOD__);
 | 
				
			||||||
            throw $this->db->getSchema()->convertException($e, $rawSql ?: $this->getRawSql());
 | 
					            throw $e;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1064,7 +1074,7 @@ class Command extends Component
 | 
				
			|||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $profile and Yii::beginProfile($rawSql, 'yii\db\Command::query');
 | 
					            $profile and Yii::beginProfile($rawSql, 'yii\db\Command::query');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $this->pdoStatement->execute();
 | 
					            $this->internalExecute($rawSql);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if ($method === '') {
 | 
					            if ($method === '') {
 | 
				
			||||||
                $result = new DataReader($this);
 | 
					                $result = new DataReader($this);
 | 
				
			||||||
@ -1077,9 +1087,9 @@ class Command extends Component
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $profile and Yii::endProfile($rawSql, 'yii\db\Command::query');
 | 
					            $profile and Yii::endProfile($rawSql, 'yii\db\Command::query');
 | 
				
			||||||
        } catch (\Exception $e) {
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
            $profile and Yii::endProfile($rawSql, 'yii\db\Command::query');
 | 
					            $profile and Yii::endProfile($rawSql, 'yii\db\Command::query');
 | 
				
			||||||
            throw $this->db->getSchema()->convertException($e, $rawSql ?: $this->getRawSql());
 | 
					            throw $e;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (isset($cache, $cacheKey, $info)) {
 | 
					        if (isset($cache, $cacheKey, $info)) {
 | 
				
			||||||
@ -1114,7 +1124,78 @@ class Command extends Component
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Resets [[sql]] and [[params]] properties.
 | 
					     * Marks the command to be executed in transaction.
 | 
				
			||||||
 | 
					     * @param string|null $isolationLevel The isolation level to use for this transaction.
 | 
				
			||||||
 | 
					     * See [[Transaction::begin()]] for details.
 | 
				
			||||||
 | 
					     * @return $this this command instance.
 | 
				
			||||||
 | 
					     * @since 2.0.14
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected function requireTransaction($isolationLevel = null)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->_isolationLevel = $isolationLevel;
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Sets a callable (e.g. anonymous function) that is called when [[Exception]] is thrown
 | 
				
			||||||
 | 
					     * when executing the command. The signature of the callable should be:
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * ```php
 | 
				
			||||||
 | 
					     * function (\yii\db\Exception $e, $attempt)
 | 
				
			||||||
 | 
					     * {
 | 
				
			||||||
 | 
					     *     // return true or false (whether to retry the command or rethrow $e)
 | 
				
			||||||
 | 
					     * }
 | 
				
			||||||
 | 
					     * ```
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param callable $handler a PHP callback to handle database exceptions.
 | 
				
			||||||
 | 
					     * @return $this this command instance.
 | 
				
			||||||
 | 
					     * @since 2.0.14
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected function setRetryHandler(callable $handler)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->_retryHandler = $handler;
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Executes a prepared statement.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * It's a wrapper around [[\PDOStatement::execute()]] to support transactions
 | 
				
			||||||
 | 
					     * and retry handlers.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string|null $rawSql the rawSql if it has been created.
 | 
				
			||||||
 | 
					     * @throws Exception if execution failed.
 | 
				
			||||||
 | 
					     * @since 2.0.14
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected function internalExecute($rawSql)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $attempt = 0;
 | 
				
			||||||
 | 
					        while (true) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    ++$attempt === 1
 | 
				
			||||||
 | 
					                    && $this->_isolationLevel !== false
 | 
				
			||||||
 | 
					                    && $this->db->getTransaction() === null
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                    $this->db->transaction(function () use ($rawSql) {
 | 
				
			||||||
 | 
					                        $this->internalExecute($rawSql);
 | 
				
			||||||
 | 
					                    }, $this->_isolationLevel);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    $this->pdoStatement->execute();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            } catch (\Exception $e) {
 | 
				
			||||||
 | 
					                $rawSql = $rawSql ?: $this->getRawSql();
 | 
				
			||||||
 | 
					                $e = $this->db->getSchema()->convertException($e, $rawSql);
 | 
				
			||||||
 | 
					                if ($this->_retryHandler === null || !call_user_func($this->_retryHandler, $e, $attempt)) {
 | 
				
			||||||
 | 
					                    throw $e;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Resets command properties to their initial state.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @since 2.0.13
 | 
					     * @since 2.0.13
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@ -1124,5 +1205,7 @@ class Command extends Component
 | 
				
			|||||||
        $this->_pendingParams = [];
 | 
					        $this->_pendingParams = [];
 | 
				
			||||||
        $this->params = [];
 | 
					        $this->params = [];
 | 
				
			||||||
        $this->_refreshTableName = null;
 | 
					        $this->_refreshTableName = null;
 | 
				
			||||||
 | 
					        $this->_isolationLevel = false;
 | 
				
			||||||
 | 
					        $this->_retryHandler = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ namespace yiiunit\framework\db;
 | 
				
			|||||||
use yii\caching\FileCache;
 | 
					use yii\caching\FileCache;
 | 
				
			||||||
use yii\db\Connection;
 | 
					use yii\db\Connection;
 | 
				
			||||||
use yii\db\DataReader;
 | 
					use yii\db\DataReader;
 | 
				
			||||||
 | 
					use yii\db\Exception;
 | 
				
			||||||
use yii\db\Expression;
 | 
					use yii\db\Expression;
 | 
				
			||||||
use yii\db\Schema;
 | 
					use yii\db\Schema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1129,4 +1130,50 @@ SQL;
 | 
				
			|||||||
        $db->createCommand()->dropTable($tableName)->execute();
 | 
					        $db->createCommand()->dropTable($tableName)->execute();
 | 
				
			||||||
        $this->assertNull($db->getSchema()->getTableSchema($tableName));
 | 
					        $this->assertNull($db->getSchema()->getTableSchema($tableName));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @group iss
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function testTransaction()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $connection = $this->getConnection(false);
 | 
				
			||||||
 | 
					        $this->assertNull($connection->transaction);
 | 
				
			||||||
 | 
					        $command = $connection->createCommand("INSERT INTO {{profile}}([[description]]) VALUES('command transaction')");
 | 
				
			||||||
 | 
					        $this->invokeMethod($command, 'requireTransaction');
 | 
				
			||||||
 | 
					        $command->execute();
 | 
				
			||||||
 | 
					        $this->assertNull($connection->transaction);
 | 
				
			||||||
 | 
					        $this->assertEquals(1, $connection->createCommand("SELECT COUNT(*) FROM {{profile}} WHERE [[description]] = 'command transaction'")->queryScalar());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @group iss
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function testRetryHandler()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $connection = $this->getConnection(false);
 | 
				
			||||||
 | 
					        $this->assertNull($connection->transaction);
 | 
				
			||||||
 | 
					        $connection->createCommand("INSERT INTO {{profile}}([[description]]) VALUES('command retry')")->execute();
 | 
				
			||||||
 | 
					        $this->assertNull($connection->transaction);
 | 
				
			||||||
 | 
					        $this->assertEquals(1, $connection->createCommand("SELECT COUNT(*) FROM {{profile}} WHERE [[description]] = 'command retry'")->queryScalar());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $attempts = null;
 | 
				
			||||||
 | 
					        $hitHandler = false;
 | 
				
			||||||
 | 
					        $hitCatch = false;
 | 
				
			||||||
 | 
					        $command = $connection->createCommand("INSERT INTO {{profile}}([[id]], [[description]]) VALUES(1, 'command retry')");
 | 
				
			||||||
 | 
					        $this->invokeMethod($command, 'setRetryHandler', [function ($exception, $attempt) use (&$attempts, &$hitHandler) {
 | 
				
			||||||
 | 
					            $attempts = $attempt;
 | 
				
			||||||
 | 
					            $hitHandler = true;
 | 
				
			||||||
 | 
					            return $attempt <= 2;
 | 
				
			||||||
 | 
					        }]);
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $command->execute();
 | 
				
			||||||
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
 | 
					            $hitCatch = true;
 | 
				
			||||||
 | 
					            $this->assertInstanceOf('yii\db\IntegrityException', $e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $this->assertNull($connection->transaction);
 | 
				
			||||||
 | 
					        $this->assertSame(3, $attempts);
 | 
				
			||||||
 | 
					        $this->assertTrue($hitHandler);
 | 
				
			||||||
 | 
					        $this->assertTrue($hitCatch);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user