使用 PDO 进行数据库事务

数据库事务确保只有在每个语句成功时才会使一组数据更改成为永久更改。可以捕获事务期间的任何查询或代码失败,然后你可以选择回滚尝试的更改。

PDO 提供了用于开始,提交和回滚事务的简单方法。

$pdo = new PDO(
    $dsn, 
    $username, 
    $password, 
    array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
);

try {
    $statement = $pdo->prepare("UPDATE user SET name = :name");

    $pdo->beginTransaction();

    $statement->execute(["name"=>'Bob']);
    $statement->execute(["name"=>'Joe']);

    $pdo->commit();
} 
catch (\Exception $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollback();
        // If we got here our two data updates are not in the database
    }
    throw $e;
}

在事务期间,所做的任何数据更改仅对活动连接可见。SELECT 语句将返回已更改的更改,即使它们尚未提交到数据库。

:有关事务支持的详细信息,请参阅数据库供应商文有些系统根本不支持交易。有些支持嵌套事务,有些则不支持。

使用 PDO 进行事务的实际示例

在下一节中演示了一个实际的现实世界示例,其中使用事务确保数据库的一致性。

想象一下以下场景,假设你正在为电子商务网站构建购物车,并且你决定将订单保存在两个数据库表中。一个名为 orders,字段为 order_idnameaddresstelephonecreated_at。第二个名为 orders_products,字段为 order_idproduct_idquantity。第一个表包含订单的元数据,第二个表包含已订购的实际产品

将新订单插入数据库

要在数据库中插入新订单,你需要做两件事。首先,你需要在 orders 表中创建一条新记录,其中包含订单的元数据nameaddress 等)。然后,你需要将一条记录发送到 orders_products 表中,包含在订单中的每个产品中。

你可以通过执行类似以下操作来执行此操作:

// Insert the metadata of the order into the database
$preparedStatement = $db->prepare(
    'INSERT INTO `orders` (`name`, `address`, `telephone`, `created_at`)
     VALUES (:name, :address, :telephone, :created_at)'
);

$preparedStatement->execute([
    'name' => $name,
    'address' => $address,
    'telephone' => $telephone,
    'created_at' => time(),
]);

// Get the generated `order_id`
$orderId = $db->lastInsertId();

// Construct the query for inserting the products of the order
$insertProductsQuery = 'INSERT INTO `orders_products` (`order_id`, `product_id`, `quantity`) VALUES';

$count = 0;
foreach ( $products as $productId => $quantity ) {
    $insertProductsQuery .= ' (:order_id' . $count . ', :product_id' . $count . ', :quantity' . $count . ')';
    
    $insertProductsParams['order_id' . $count] = $orderId;
    $insertProductsParams['product_id' . $count] = $productId;
    $insertProductsParams['quantity' . $count] = $quantity;
    
    ++$count;
}

// Insert the products included in the order into the database
$preparedStatement = $db->prepare($insertProductsQuery);
$preparedStatement->execute($insertProductsParams);

这对于将新订单插入数据库非常有用,直到出现意外情况并且由于某种原因第二个 INSERT 查询失败。如果发生这种情况,你最终会在 orders 表中找到一个新订单,该订单中没有与之关联的产品。幸运的是,修复非常简单,你所要做的就是以单个数据库事务的形式进行查询。

使用事务将新订单插入数据库

要使用 PDO 启动事务,你所要做的就是在对数据库执行任何查询之前调用 beginTransaction 方法。然后,通过执行 INSERT 和/或 UPDATE 查询,你可以对数据进行任何更改。最后,你调用 PDO 对象的 commit 方法使更改成为永久更改。在你调用 commit 方法之前,你对数据所做的每一项更改都不是永久性的,只需调用 PDO 对象的 rollback 方法即可轻松恢复。

在下面的示例中演示了使用事务将新订单插入数据库,同时确保数据的一致性。如果两个查询中的一个失败,则将还原所有更改。

// In this example we are using MySQL but this applies to any database that has support for transactions
$db = new PDO('mysql:host=' . $host . ';dbname=' . $dbname . ';charset=utf8', $username, $password);    

// Make sure that PDO will throw an exception in case of error to make error handling easier
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

try {
    // From this point and until the transaction is being committed every change to the database can be reverted
    $db->beginTransaction();    
    
    // Insert the metadata of the order into the database
    $preparedStatement = $db->prepare(
        'INSERT INTO `orders` (`order_id`, `name`, `address`, `created_at`)
         VALUES (:name, :address, :telephone, :created_at)'
    );
    
    $preparedStatement->execute([
        'name' => $name,
        'address' => $address,
        'telephone' => $telephone,
        'created_at' => time(),
    ]);
    
    // Get the generated `order_id`
    $orderId = $db->lastInsertId();

    // Construct the query for inserting the products of the order
    $insertProductsQuery = 'INSERT INTO `orders_products` (`order_id`, `product_id`, `quantity`) VALUES';
    
    $count = 0;
    foreach ( $products as $productId => $quantity ) {
        $insertProductsQuery .= ' (:order_id' . $count . ', :product_id' . $count . ', :quantity' . $count . ')';
        
        $insertProductsParams['order_id' . $count] = $orderId;
        $insertProductsParams['product_id' . $count] = $productId;
        $insertProductsParams['quantity' . $count] = $quantity;
        
        ++$count;
    }
    
    // Insert the products included in the order into the database
    $preparedStatement = $db->prepare($insertProductsQuery);
    $preparedStatement->execute($insertProductsParams);
    
    // Make the changes to the database permanent
    $db->commit();
}
catch ( PDOException $e ) { 
    // Failed to insert the order into the database so we rollback any changes
    $db->rollback();
    throw $e;
}