Laravel控制器单方法中多模型增改的原子性处理问题
解决Laravel多模型操作的原子性问题:用数据库事务保证数据一致性
嘿,这个问题其实Laravel内置的数据库事务就能完美解决!咱们的核心思路就是把所有需要保证原子性的数据库操作包裹在事务中——只要其中任何一步出现错误(比如模型验证失败、数据库查询异常、找不到关联模型等),整个事务就会自动回滚,所有已经执行的插入/更新操作都会被撤销,彻底保证数据一致性。
核心方案:使用DB::transaction()闭包
Laravel的DB::transaction()方法支持闭包形式,只要闭包内抛出任何异常,事务就会自动回滚;如果闭包顺利执行完成,事务就会自动提交,非常省心!
修改后的完整控制器代码
我给你调整了原代码,重点标注了事务相关的修改,还优化了几个容易触发异常的点:
use Illuminate\Support\Facades\DB; use Illuminate\Database\Eloquent\ModelNotFoundException; public function store(Request $request) { $this->authorize('loans.create'); $this->validate($request, [ 'loan_id' => 'required|numeric', 'received_by' => 'required|string|max:100', 'received_amount' => 'required|numeric', 'deposit' => 'nullable|numeric', 'deposit_1' => 'numeric|nullable', 'installments' => 'required|numeric', 'date' => 'required|date', 'total_amount' => 'required|numeric', ]); try { // 开启数据库事务:所有核心数据库操作都在这个闭包里执行 $transactionData = DB::transaction(function () use ($request) { // 1. 创建Loan模型 $loan = new Loan(); $loan->loan_id = $request->loan_id; $loan->received_by = $request->received_by; $loan->received_amount = $request->received_amount; $loan->deposit = $request->deposit; $loan->deposit_1 = $request->deposit_1; $loan->installments = $request->installments; $loan->date = $request->date; $loan->Txn = Str::random(10); $loan->total_amount = $request->total_amount; $loan->somity_id = auth()->user()->mst_somity_id; $loan->is_active = 1; $loan->created_user_id = auth()->user()->id; // 补上原代码遗漏的模型赋值 $loan->save(); // 2. 更新AssignLoan模型:用findOrFail代替find,找不到时自动抛出异常触发回滚 $AssignLoan = AssignLoan::findOrFail($request->loan_id); $AssignLoan->deposit += $request->deposit; $AssignLoan->deposit_1 += $request->deposit_1; $AssignLoan->paid_amount += $request->received_amount; $AssignLoan->remaining_amount = $AssignLoan->total_amount - $request->received_amount; $AssignLoan->save(); // 3. 创建TransactionReport模型 $TransactionReport = new TransactionReport(); $TransactionReport->transaction_type = 'loan_received'; $TransactionReport->customer_id = $AssignLoan->customer_id; // 直接用关联模型更可靠 $TransactionReport->transaction_description = 'loan received with deposit'; $TransactionReport->transaction_method = 'cash'; $TransactionReport->transaction_amount = $request->total_amount; $TransactionReport->transaction_date = $request->date; $TransactionReport->transaction_time = date('H:i:s'); $TransactionReport->transaction_by = $request->received_by; $TransactionReport->somity_id = auth()->user()->mst_somity_id; $TransactionReport->created_user_id = auth()->user()->id; $TransactionReport->is_loan_diposite = $loan->id; $TransactionReport->Txn = $loan->Txn; $TransactionReport->save(); // 将需要用到的模型传递到闭包外 return [ 'loan' => $loan, 'AssignLoan' => $AssignLoan ]; }); // 从返回值中取出模型(短信逻辑需要) $AssignLoan = $transactionData['AssignLoan']; // 只有当事务成功提交后,才执行短信相关操作(外部API不能放在事务里) $AdminSeetings = AdminSeetings::where('somity_id', auth()->user()->mst_somity_id)->first(); if($AdminSeetings?->automatic_sms == 1){ // 用null安全访问避免空对象错误 $SmsPackageAssignToSomity = SmsPackageAssignToSomity::where('somity_id', auth()->user()->mst_somity_id)->first(); if($SmsPackageAssignToSomity?->remaining_sms > 0){ $customer_number = $AssignLoan->customer->customer_phone; $loan_id = $AssignLoan->loan_number; $loan_amount = $request->received_amount; $remaining_amount = $AssignLoan->remaining_amount; $message = "Your loan No: ". $loan_id . ". received amount: ". $loan_amount ." Tk. Remaining Balance: ". $remaining_amount ." Tk."; // 扣减短信余额(若需和核心业务强一致,也可将此操作加入上方事务) $SmsPackageAssignToSomity->remaining_sms -= 1; $SmsPackageAssignToSomity->use_sms += 1; $SmsPackageAssignToSomity->save(); // 发送短信 $url = "https://bulksms.aloitltd.com/services/send.php?key=".env('API_KEY')."&number=" . $customer_number . "&message=". $message . "&option=".env('OPTION')."&type=".env('TYPE')."&prioritize=".env('PRIORITIZE'); return Redirect::to($url); } else { $notification = [ 'message' => 'Loan Added Successfully and SMS LIMIT EXCEED', 'alert-type' => 'success' ]; return redirect()->route('loan.index')->with($notification); } } else { $notification = [ 'message' => 'Loan Added Successfully', 'alert-type' => 'success' ]; return redirect()->route('loan.index')->with($notification); } } catch (ModelNotFoundException $e) { // 处理找不到指定贷款记录的情况 $notification = [ 'message' => '指定的贷款记录不存在', 'alert-type' => 'error' ]; return redirect()->back()->with($notification); } catch (\Exception $e) { // 处理其他所有异常,返回友好提示 $notification = [ 'message' => '操作失败:'.$e->getMessage(), 'alert-type' => 'error' ]; return redirect()->back()->with($notification); } }
关键修改点说明
- 事务包裹核心数据库操作:把
Loan创建、AssignLoan更新、TransactionReport创建这三个核心业务操作全部放到DB::transaction()的闭包里,确保它们要么全部成功,要么全部回滚。 - 用
findOrFail()替代find():原代码用find()如果找不到模型会返回null,后续调用->save()会报错但不会自动触发事务回滚;findOrFail()找不到时会直接抛出ModelNotFoundException,事务会自动回滚。 - 外部操作移到事务之后:短信发送是外部API调用,不属于数据库操作,必须放到事务提交成功后执行——否则如果事务回滚了,但短信已经发出去,就会出现数据和业务不一致的情况。
- 异常捕获:增加了分层异常捕获,给用户返回友好的错误提示,同时方便开发时排查问题。
- 修复小bug:原代码里
$loan->created_user_id只定义了变量但没赋值给模型,我补上了这个赋值。
这样修改后,你的代码就能完美保证操作的原子性啦!
内容的提问来源于stack exchange,提问作者IslamYearul
相关产品推荐
相关产品推荐

