Laravel:如何在DB::transaction()中使用try...catch语句

136

我们都使用 DB::transaction() 来进行多次插入查询。在这样做时,应该将 try...catch 放在其中还是包装它?如果事务出了问题会自动失败,是否有必要包含 try...catch 呢?

事务包装的示例 try...catch

// try...catch
try {
    // Transaction
    $exception = DB::transaction(function() {

        // Do your SQL here

    });

    if(is_null($exception)) {
        return true;
    } else {
        throw new Exception;
    }

}
catch(Exception $e) {
    return false;
}

相反,一个包装在try...catch中的DB::transaction()

// Transaction
$exception = DB::transaction(function() {
    // try...catch
    try {

        // Do your SQL here

    }
    catch(Exception $e) {
        return $e;
    }

});

return is_null($exception) ? true : false;

或者仅仅是没有 try...catch 的交易

// Transaction only
$exception = DB::transaction(function() {

    // Do your SQL here

});

return is_null($exception) ? true : false;
8个回答

302

如果您需要通过代码手动“退出”一个事务(无论是通过异常还是仅检查错误状态),则不应该使用 DB::transaction(),而是应该将代码包装在 DB::beginTransactionDB::commit/DB::rollback() 中:

DB::beginTransaction();

try {
    DB::insert(...);
    DB::insert(...);
    DB::insert(...);

    DB::commit();
    // all good
} catch (\Exception $e) {
    DB::rollback();
    // something went wrong
}

查看事务文档


DB::beginTransaction()DB:transaction() 有什么区别? - Hamed Kamrava
3
简单问题:如果在异常之后不进行回滚,或者不捕获异常会发生什么?脚本结束后自动回滚吗? - neoteknic
2
@HengSopheak 这个问题是关于 Laravel 4 数据库的,所以我的答案对于 5.3 版本可能不再正确。你可以提一个新问题,并加上 Laravel 5.3 标签来获取正确的社区支持。 - alexrussell
我已经尝试了很多关于Laravel5.3.19的数据库事务,但它并没有像预期的那样工作。现在我正在尝试更多次来验证我的实践,并且我有更多关于这个问题的教程,但他们说这不是一个错误。让我再试一次。但我非常确定这是因为我通过公司更新和安装了最新版本的LR框架。 - DMS-KH
我使用了Db:commit,但它似乎不起作用,我该如何让它起作用? - Freddy Sidauruk
显示剩余12条评论

42
如果您使用PHP7,请在catch中使用Throwable来捕获用户异常和致命错误。
例如:
DB::beginTransaction();

try {
    DB::insert(...);    
    DB::commit();
} catch (\Throwable $e) {
    DB::rollback();
    throw $e;
}

如果您的代码必须与PHP5兼容,请使用ExceptionThrowable

DB::beginTransaction();

try {
    DB::insert(...);    
    DB::commit();
} catch (\Exception $e) {
    DB::rollback();
    throw $e;
} catch (\Throwable $e) {
    DB::rollback();
    throw $e;
}

DB::beginTransaction() 也可能会抛出 \Exception 异常,那么它应该包含在 try/catch 中吗? - Michael Pawlowsky
4
如果事务还未开始,那么我们不需要回滚任何东西。此外,在 catch 块中尝试回滚未启动的事务是无效的。因此,DB::beginTransaction() 的好位置是在 try 块之前。 - Nick

23

你可以在try..catch中包装交易,甚至可以将它们反向执行, 这里是我在laravel 5中使用的示例代码,如果你深入研究Illuminate\Database\Connection中的DB:transaction(),就会发现它与手动事务的编写方式相同。

Laravel事务

public function transaction(Closure $callback)
    {
        $this->beginTransaction();

        try {
            $result = $callback($this);

            $this->commit();
        }

        catch (Exception $e) {
            $this->rollBack();

            throw $e;
        } catch (Throwable $e) {
            $this->rollBack();

            throw $e;
        }

        return $result;
    }

所以你可以像这样编写代码,并通过flash将异常消息返回到你的表单中,或者重定向到另一个页面处理。记住,闭包内的return在transaction()中返回,所以如果你返回redirect()->back(),它不会立即重定向,因为它返回了一个变量来处理事务。

包装Transaction

try {
    $result = DB::transaction(function () use ($request, $message) {
        // execute query 1
        // execute query 2
        // ..
    });          
    // redirect the page
    return redirect(route('account.article'));
} catch (\Exception $e) {
    return redirect()->back()->withErrors(['error' => $e->getMessage()]);
}

如果不使用try-catch语句包裹代码块,那么可以在事务函数外部抛出布尔变量并处理重定向;或者,如果您需要获取事务失败的原因,可以在catch(Exception $e){...}中通过$e->getMessage()获得。


我在没有try-catch块的情况下使用了事务,也很好地运行了。 - hamidreza samsami
@hamidrezasamsami 是的,数据库自动回滚了,但有时您需要知道查询是否全部成功。 - Angga Ari Wijaya
10
“Wrap Transaction”示例有误。即使其中一个查询失败,此示例始终会提交,因为所有异常都在事务回调中捕获。你需要将try/catch放在DB::transaction之外。 - redmallard
在2022年1月20日更新了“Wrap Transaction”代码以反映@redmallard的建议后,我认为这应该是正确的答案。此外,我认为异常/可抛出的二分法大多浪费时间,因此将所有处理程序编写为catch(\Exception $e){...}并使用前导反斜杠来避免歧义。似乎\Throwable更适用于框架内部,但如果有人有合法的用例,在此处发表评论将会有所帮助。 - Zack Morris
但是 DB::beginTransaction 也会抛出 Throwable,所以基于这个,它不应该也在 try {} 内部吗? - Lucas M. Oliveira

9
我决定回答这个问题,因为我认为可以使用比复杂的try-catch块更简单的语法来解决它。Laravel文档在这个主题上很简短。
您可以像这样使用DB::transaction(){...}包装器,而不是使用try-catch:
// MyController.php
public function store(Request $request) {
    return DB::transaction(function() use ($request) {
        $user = User::create([
            'username' => $request->post('username')
        ]);

        // Add some sort of "log" record for the sake of transaction:
        $log = Log::create([
            'message' => 'User Foobar created'
        ]);

        // Lets add some custom validation that will prohibit the transaction:
        if($user->id > 1) {
            throw AnyException('Please rollback this transaction');
        }

        return response()->json(['message' => 'User saved!']);
    });
};

在这种设置中,您应该注意到用户和日志记录是相互依存的。

上述实现的一些注意事项:

  • 请确保在事务中 return 任何内容,以便您可以将您返回的 response() 用作控制器的响应。
  • 如果希望回滚事务(或者像Eloquent中的任何SQL异常一样,有一个自动抛出异常的嵌套函数),请务必 throw 一个异常。
  • idupdated_atcreated_at 和其他任何字段都可在创建后为 $user 对象使用(至少在此事务运行期间)。事务将通过您拥有的任何创建逻辑进行处理。但是,当抛出 SomeCustomException 时,整个记录都将被丢弃。失败的事务会增加一个自增的 id 列。

已测试 Laravel 5.8


4

我正在使用Laravel 8,你应该按照以下方式将事务包装在try-catch中:

try {
    DB::transaction(function () {
        // Perform your queries here using the models or DB facade
    });
}
catch (\Throwable $e) {
    // Do something with your exception
}

1

首先:在Laravel中使用PostgreSQL数据库会使事情变得更加棘手。

如果您在事务错误后不回滚,每个进一步的查询都将抛出此错误在失败的SQL事务中:ERROR:当前事务已中止,命令被忽略直到事务块结束。因此,如果您无法在回滚之前将原始错误消息保存在表中,则需要注意。

try {
    DB::beginTransaction(); //start transaction
    $user1 = User::find(1);
    $user1->update(['money' => 'not_a_number']); //bad update
}
catch(Exception $exception) {
    $user2 = User::find(2); // ko, "In failed sql transaction" error
    $user2->update(['field' => 'value']);
}

try {
    DB::beginTransaction(); //start transaction
    $user1 = User::find(1);
    $user1->update(['money' => 'not_a_number']); //bad update
}
catch(Exception $exception) {
    DB::rollBack();
    $user2 = User::find(2); // ok, go on
    $user2->update(['field' => 'value']);
}

第二点:注意 Eloquent 模型属性系统。
Eloquent 模型在更新错误后会保留已更改的属性,因此如果我们想在 catch 块中更新该模型,我们需要丢弃错误的属性。这不是一个数据库事务问题,因此回滚命令是无用的。
try {
    DB::beginTransaction(); //start transaction
    $user1 = User::find(1);
    $user1->update(['money' => 'not_a_number']); //bad update
}
catch(Exception|Error $exception) {
    DB::rollBack();
    $user1->update(['success' => 'false']); // ko, bad update again
}

try {
    DB::beginTransaction(); //start transaction
    $user1 = User::find(1);
    $user1->update(['money' => 'not_a_number']); //bad update
}
catch(Exception|Error $exception) {
    DB::rollBack();
    $user1->discardChanges(); // remove attribute changes from model
    $user1->update(['success' => 'false']); // ok, go on
}

1
在 Laravel 8 中,您可以在 try-catch 中使用 DB::transaction。 例如:
try{
    DB::transaction(function() {
        // do anything
    });
}
catch(){
    // do anything
}

如果每个查询都在尝试失败时,将执行catch块。

我可以在catch中使用"DB::rollback"吗,尽管那里没有事务?或者为了使用它,我需要将"DB:transaction"移到"try"之前吗? - Carlos
我编辑了我的帖子来回答Carlos的评论。 - undefined

0
我建议你去config/database.php文件中将引擎更改为InnoDB。
MyISAM是MySQL数据库的默认引擎,但它不支持事务。通过在配置文件中添加InnoDB引擎,可以确保以后创建的所有表都将使用它作为默认引擎。
  <?php

use Illuminate\Support\Str;

return [

  // Other settings we don't care about.

    'connections' => [
      
        // Other connections we don't care about

        'mysql' => [
            // Other mysql configurations we don't care about
            'engine' => 'InnoDB',
        ],
    ]
];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接