SELECT FOR UPDATE(技巧)(有点丑陋,但有效)(PostgreSQL 和 MySql)

有时我们需要执行 SELECT … FOR UPDATE 以维护事务的完整性。目前 CakePHP 模型查找操作不支持此功能。以下是一个“技巧”,可以省去编码时间,尤其当你像我一样赶时间时,或者你不介意为我们所有人编写一个有用的行为。

有时我们需要执行 SELECT … FOR UPDATE 以维护事务的完整性。以下示例可以清楚地说明问题。

假设我们有一个用户,一个 account 表,其中包含 account.available_money 字段。假设用户最初有 100 美元。

如果黑客能够同时运行两个进程(只显示 find()save() 生成的相关查询),就会出现问题。

一个进程执行

BEGIN
SELECT available_money from account where user_id = '1' ; -- $money = $100
UPDATE account SET available_money = 0 where user_id = '1'; -- the user buy $100 and depletes his account
COMMIT

而另一个同时运行的进程执行

-- php process _also_ finds $100 money so the user can user can use the $100 again it _again_
BEGIN
SELECT available_money from account  where user_id = '1'; // $money = $100
UPDATE account SET available_money = 0 where user_id = '1'; //the user buys _again_ $100 and he depletes his account
COMMIT

如果发生这种情况,用户可能会使用超出其限额的资金。SELECT FOR UPDATE 就能解决这个问题。延续之前的示例。

一个进程执行

BEGIN
SELECT available_money from account  where user_id = '1' FOR UPDATE; // $money = $100
UPDATE account SET available_money = 0 where user_id = '1';
COMMIT

而另一个同时运行的进程执行

BEGIN
SELECT available_money from account where user_id = '1'; // now this second process must wait the first process to finish, so $money = $0
-- the user doest not have money to buy anything
COMMIT

那么如何使用 CakePHP 实现这一点呢?

CakePHP 的 dbo 目前不支持 FOR UPDATE 语法。但是,有一个快速且肮脏的(可能对某些人来说过于肮脏)技巧,你可以不修改核心代码就实现它……

在 dbo_source 中,SELECT 语句中的 LIMIT 子句是最后解析的(参见 cake/libs/models/dbo_source.php 中的第 1497 行)。

switch (strtolower($type)) {
    case 'select':
        return "SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}";
    break;
    //...

如果需要,并且使用 PostgreSQL(稍后介绍 mysql),你可以执行以下操作

$transac = $this->AccountModel->find('all', array(
    'conditions' => array(
        'user_id' =>$id
    ),
    'fields' => array('available_money'),
    'limit' => 'ALL FOR UPDATE'
));`

… 这将生成我们正在寻找的 SELECT available_money from account where user_id = ‘1’ FOR UPDATE 子句… 技巧在于 ‘limit’ => ‘ALL FOR UPDATE’。这会选择满足条件的所有记录以进行更新。

对于 MySql,你需要执行一些更丑陋的操作。由于 MySql 不支持 LIMIT ALL,你需要使用 LIMIT 0,18446744073709551615。我知道这很糟糕…… 另一个选择是为我们所有人编写一个行为:)

请记住将你的 find 包含在事务中!

注意,这里有一个小而重要的陷阱… 你不能执行 find(‘first’),因为 LIMIT 子句会被 Cake 覆盖为 LIMIT 1,而你可以执行 find(‘all’, … ‘limit’ => ‘1 FOR UPDATE’ …)。