PHP Conference Japan 2024

PDO::beginTransaction

(PHP 5 >= 5.1.0, PHP 7, PHP 8, PECL pdo >= 0.1.0)

PDO::beginTransaction 启动一个事务

说明

public PDO::beginTransaction(): bool

关闭自动提交模式。在关闭自动提交模式时,通过 PDO 对象实例对数据库进行的更改直到调用 PDO::commit() 结束事务才会提交。调用 PDO::rollBack() 将回滚对数据库进行的所有更改并将连接返回到自动提交模式。

某些数据库(包括 MySQL)在事务中发出诸如 DROP TABLE 或 CREATE TABLE 之类的数据库定义语言 (DDL) 语句时,会自动发出隐式 COMMIT。隐式 COMMIT 将阻止您回滚事务边界内的任何其他更改。

参数

此函数没有参数。

返回值

成功时返回 true,失败时返回 false

错误/异常

如果已经启动了一个事务或驱动程序不支持事务,则抛出一个 PDOException

注意: 即使 PDO::ATTR_ERRMODE 属性不是 PDO::ERRMODE_EXCEPTION,也会引发异常。

范例

示例 #1 回滚事务

以下示例开始一个事务并在回滚更改之前发出两个修改数据库的语句。但是,在 MySQL 上,DROP TABLE 语句会自动提交事务,以便事务中的所有更改都不会回滚。

<?php
/* 开始一个事务,关闭自动提交 */
$dbh->beginTransaction();

/* 更改数据库模式和数据 */
$sth = $dbh->exec("DROP TABLE fruit");
$sth = $dbh->exec("UPDATE dessert
SET name = 'hamburger'"
);

/* 识别错误并回滚更改 */
$dbh->rollBack();

/* 数据库连接现在恢复为自动提交模式 */
?>

参见

添加注释

用户贡献的注释 12 条注释

56
steve at fancyguy dot com
9 年前
此处的嵌套事务示例非常棒,但它缺少一个关键部分。提交将提交所有内容,我只希望在最外层提交完成后才实际提交。这可以在 InnoDB 中使用保存点完成。

<?php

class Database extends PDO
{

protected
$transactionCount = 0;

public function
beginTransaction()
{
if (!
$this->transactionCounter++) {
return
parent::beginTransaction();
}
$this->exec('SAVEPOINT trans'.$this->transactionCounter);
return
$this->transactionCounter >= 0;
}

public function
commit()
{
if (!--
$this->transactionCounter) {
return
parent::commit();
}
return
$this->transactionCounter >= 0;
}

public function
rollback()
{
if (--
$this->transactionCounter) {
$this->exec('ROLLBACK TO trans'.$this->transactionCounter + 1);
return
true;
}
return
parent::rollback();
}

}
31
bitluni
12 年前
您可以使用嵌套的 beginTransaction 和 commit 调用生成问题。
示例

beginTransaction()
执行重要操作
调用方法
beginTransaction()
基本操作 1
基本操作 2
commit()
执行最重要操作
commit()

无法正常工作并且很危险,因为您可以使用嵌套的 commit() 过早地关闭事务。

无需弄乱您的代码并传递类似 bool 的值来指示事务是否已经在运行。您只需在 PDO 包装器中重载 beginTransaction() 和 commit(),如下所示

<?php
class Database extends \\PDO
{
protected
$transactionCounter = 0;
function
beginTransaction()
{
if(!
$this->transactionCounter++)
return
parent::beginTransaction();
return
$this->transactionCounter >= 0;
}

function
commit()
{
if(!--
$this->transactionCounter)
return
parent::commit();
return
$this->transactionCounter >= 0;
}

function
rollback()
{
if(
$this->transactionCounter >= 0)
{
$this->transactionCounter = 0;
return
parent::rollback();
}
$this->transactionCounter = 0;
return
false;
}
//...
}
?>
16
drm at melp dot nl
16 年前
回复 "Anonymous / 20-Dec-2007 03:04"

您还可以扩展 PDO 类并持有一个私有标志来检查事务是否已经开始。

class MyPDO extends PDO {
protected $hasActiveTransaction = false;

function beginTransaction () {
if ( $this->hasActiveTransaction ) {
return false;
} else {
$this->hasActiveTransaction = parent::beginTransaction ();
return $this->hasActiveTransaction;
}
}

function commit () {
parent::commit ();
$this->hasActiveTransaction = false;
}

function rollback () {
parent::rollback ();
$this->hasActiveTransaction = false;
}

}
13
kesler dot alwin at gmail dot com
8 年前
请修复 #116669 的回答

$this->exec('ROLLBACK TO trans'.$this->transactionCounter + 1);

改为

$this->exec('ROLLBACK TO trans'.($this->transactionCounter + 1));
12
rjohnson at intepro dot us
15 年前
如果您正在使用 PDO::SQLITE 并且需要支持具有锁定的高并发级别,请尝试在调用 beginTransaction() 之前预处理您的语句,并且您可能还需要在 SELECT 语句上调用 closeCursor() 以防止驱动程序认为存在打开的事务。

这是一个示例(Windows,PHP 版本 5.2.8)。我们通过打开此脚本的 2 个浏览器选项卡并同时运行它们来测试此问题。如果我们将 beginTransaction 放在 prepare 之前,则第二个浏览器选项卡将命中 catch 块,并且 commit 将抛出另一个指示事务仍处于打开状态的 PDOException。

<?php
$conn
= new PDO('sqlite:C:\path\to\file.sqlite');
$stmt = $conn->prepare('INSERT INTO my_table(my_id, my_value) VALUES(?, ?)');
$waiting = true; // 设置一个循环条件来测试
while($waiting) {
try {
$conn->beginTransaction();
for(
$i=0; $i < 10; $i++) {
$stmt->bindValue(1, $i, PDO::PARAM_INT);
$stmt->bindValue(2, 'TEST', PDO::PARAM_STR);
$stmt->execute();
sleep(1);
}
$conn->commit();
$waiting = false;
} catch(
PDOException $e) {
if(
stripos($e->getMessage(), 'DATABASE IS LOCKED') !== false) {
// 这应该是 SQLite 特有的,休眠 0.25 秒
// 然后重试。不过我们必须先提交打开的事务
$conn->commit();
usleep(250000);
} else {
$conn->rollBack();
throw
$e;
}
}
}

?>
4
cristian at crishk dot com
8 年前
好的,我正在寻找 MySQL 中“嵌套”事务的解决方案,并且正如您在 MySQL 文档中所知,它说不可能在事务中包含事务。我尝试使用这里提出的数据库类 https://php.net/manual/en/pdo.begintransaction.php,但不幸的是,这对于与控制流相关的许多事情来说都是错误的,我已经用下面的代码解决了这些问题(请参阅最后的示例,CarOwner)

<?php

class TransactionController extends \\PDO {
public static
$warn_rollback_was_thrown = false;
public static
$transaction_rollbacked = false;
public function
__construct()
{
parent :: __construct( ... connection info ... );
}
public static
$nest = 0;
public function
reset()
{
TransactionController :: $transaction_rollbacked = false;
TransactionController :: $warn_rollback_was_thrown = false;
TransactionController :: $nest = 0;
}
function
beginTransaction()
{
$result = null;
if (
TransactionController :: $nest == 0) {
$this->reset();
$result = $this->beginTransaction();
}
TransactionController :: $nest++;
return
$result;
}

public function
commit()
{

$result = null;

if (
TransactionController :: $nest == 0 &&
!
TransactionController :: $transaction_rollbacked &&
!
TransactionController :: $warn_rollback_was_thrown) {
$result = parent :: commit();
}
TransactionController :: $nest--;
return
$result;
}

public function
rollback()
{
$result = null;
if (
TransactionController :: $nest >= 0) {
if (
TransactionController :: $nest == 0) {
$result = parent :: rollback();
TransactionController :: $transaction_rollbacked = true;
}
else {
TransactionController :: $warn_rollback_was_thrown = true;
}
}
TransactionController :: $nest--;
return
$result;
}

public function
transactionFailed()
{
return
TransactionController :: $warn_rollback_was_thrown === true;
}
// 强制回滚只能在 $nest = 0 时进行
public function forceRollback()
{
if (
TransactionController :: $nest === 0) {
throws new \PDOException();
}
}
}

?>
8
ludwig dot green at gmail dot com
14 年前
请注意,您也不能使用 TRUNCATE TABLE,因为此语句将触发提交,就像 CREATE TABLE 或 DROP TABLE 一样。

最好只在事务中使用 SELECT、UPDATE 和 DELETE,所有其他语句都可能导致提交,从而破坏事务的原子性及其回滚能力。

显然,您可以使用 DELETE FROM <table> 而不是 TRUNCATE TABLE,但请注意两者之间存在差异,例如 TRUNCATE 重置 auto_increment 值,而 DELETE 不会。
3
Sbastien
3 年前
一种使用事务和预处理语句来加速批量 INSERT 的方法。

<?php

// ...

$insert = $pdo->prepare('INSERT INTO table (c1, c2, c3) VALUES (?, ?, ?)');
$bulk = 3_000; // 根据您的数据/系统进行调整
$rows = 0;

$pdo->beginTransaction();
while (
$entry = fgetcsv($fp)) {
$insert->execute($entry);
if (++
$rows % $bulk === 0) {
$pdo->commit();
$pdo->beginTransaction();
}
}
if (
$pdo->inTransaction()) { // 插入剩余行
$pdo->commit();
}
4
dbeecher at tekops dot com
16 年前
如果您需要设置 ISOLATION 级别或 LOCK MODE,则需要在调用 BeginTransaction() 之前完成...
//
// **注意** 您应该始终检查操作的结果代码并进行错误处理。此示例代码
// 假设所有调用都能正常工作,以便操作顺序准确且易于查看
//
// 这是使用 PECL PDO::INFORMIX 模块,在 fedora core 6 上运行,php 5.2.4
//
// 这是在没有 ISAM 错误指示表损坏时解决 informix -243 错误(无法在表中定位)的正确方法。如果行被锁定,则可能会发生 -243(如果表/索引等正常)。
// 下面的代码将 LOCK MODE 设置为等待 2 分钟(120 秒)
// 放弃。在此示例中,您将获得 READ COMMITTED 行,如果您不需要读取已提交
// 但只需要获取存在的任何数据(忽略锁定的行等)而不是
// "SET LOCK MODE TO WAIT 120",您可以 "SET ISOLATION TO DIRTY READ"。
// 在 informix 中,您 *必须* 管理读取方式,因为如果您有很多行,正在使用连接,则很容易触发
//
// 锁定表溢出(这会使实例停机)
// 并且有很多更新正在发生。
// 例如,
//

// e.g.,

$sql= "SELECT FIRST 50 * FROM mytable WHERE mystuff=1 ORDER BY myid"; /* 定义 SQL 查询 */

try /* 创建异常处理程序 */
{
$dbh = new PDO("informix:host=......");

if ($dbh) /* 我们连接了吗? */
{
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbh->query("SET LOCK MODE TO WAIT 120")

# ----------------
# 打开事务游标
# ----------------
if ( $dbh->beginTransaction() ) # 显式打开游标
{
try /* 打开异常处理程序 */
{
$stmt = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL));

$stmt->execute();

while ($row = $stmt->fetch(PDO::FETCH_NUM, PDO::FETCH_ORI_NEXT))
{
$data = $row[0] . "\t" . $row[1] . "\t" . $row[2] . "\t" . $row[3] . "\t" . $row[4] . "\t" . $row[5] . "\t" . $row[6] . "\t" . $row[7] . "\n" . $row[8] ;
//print $data;
print_r($row);
};

$stmt = null;
}
catch (PDOException $e)
{
print "查询失败!\n\n";

print "DBA FAIL:" . $e->getMessage();
};

$dbh->rollback(); # 中止任何更改(即 $dbh->commit())
$dbh = null; # 关闭连接
}
else
{
# 我们永远不应该到达这里,它应该转到异常处理程序
print "无法建立连接...\n\n";
};
};
}
catch (Exception $e)
{
$dbh->rollback();
echo "失败: " . $e->getMessage();
};
2
geompse at gmail dot com
14 年前
对于 Oracle,任何结构语句都将执行隐式提交。

所以:ALTER TABLE "my_table" DROP COLUMN "my_column";
无法回滚!

希望这将为其他人节省时间
1
eddi13
10 年前
在 TRUNCATE TABLE `table` 之后就像 DELETE FROM `table` 一样,因此如果整个表被删除,则中止事务。并且回滚将不可能。
1
Steel Brain
10 年前
这个例子具有误导性,通常数据定义语言 (DDL) 子句将触发数据库引擎自动提交。这意味着如果您删除一个表,无论事务如何,该查询都将被执行。
参考-Mysql
https://dev.mysqlserver.cn/doc/refman/5.0/en/implicit-commit.html
To Top