مدیریت بهتر Database Transactions در لاراول ۸

استفاده از database transactions یک روش قدرتمند برای اطمینان از یکپارچگی داده است. شما چندین کوئری را در یک transaction قرار می‌دهید و این کوئری‌ها تنها در صورت موفقیت تمام آن‌‌ها اعمال می‌شود، کد زیر را در نظر بگیرید:

$user = User::create([...]);

Team::create([
    'owner_id' => $user->id,
    ...
]);

اگر ایجاد تیم با خطا مواجه شود، کاربر در سیستم شما بدون هیچ تیمی می‌باشد.برای جلوگیری از این اتفاق شما میتوانید کد را داخل transaction قرار بدهید:

DB::transaction(function(){
    $user = User::create([...]);

    Team::create([
        'owner_id' => $user->id,
        ...
    ]);
});

حالا اگر ایجاد تیم با خطا مواجه شود تمام transaction از جمله ایجاد کاربر بازگردانده می‌شوند. لاراول با همین چند خط کد ساده از همه چیز مراقبت می‌کند، بنابراین می‌توانید از یکپارچگی کد خود اطمینان داشته باشید.

با این حال database transactions فقط کوئری‌های دیتابیس را در نظر می‌گیرند. هر کد دیگری که شما داخل transaction قرار داده‌اید بلافاصله اجرا می‌شود و منتظر کامیت شدن transaction نمی‌ماند:

DB::transaction(function(){
    $user = User::create([...]);

    Mail::to($user)->send(new WelcomeEmail());

    Team::create([
        'owner_id' => $user->id,
        ...
    ]);
});

در مثال بالا، حتی اگر ایجاد تیم با خطا مواجه شود و (در نتیجه ) هیچ کاربری هم در دیتابیس ذخیره نشود، ایمیل خوش‌آمد گویی برای کاربر ارسال می‌شود.

یک راه‌حل ساده برای حل این مشکل خارج کردن کد ایمیل از transaction می‌باشد:

DB::transaction(function(){
    $user = User::create([...]);

    Team::create([
        'owner_id' => $user->id,
        ...
    ]);
});

Mail::to($user)->send(new WelcomeEmail());

حالا اگر transaction با خطا مواجه شود، یک exception برگشت داده می‌شود و ایمیل ارسال نمی‌شود. با این حال در بسیاری از موارد کدی که پیرامون کوئری‌های دیتابیس اجرا می‌شود مستقیما فراخوانی نمی‌شود. برای مثال، ممکن است ارسال ایمیل داخل یک listener برای ایونت UserCreated باشد که بعد از ()User::create فراخوانی می‌شود. در این مورد ارسال ایمیل دقیقا بعد از ایجاد کاربر و قبل از کامیت شدن transaction انجام می‌شود.

از لاراول v8.19.0 ، می‌توانید هر کدی را داخل یک closure قرار بدهید، که فقط پس از کامیت شدن تمام transactionها انجام می‌شود. بنابراین داخل event listenerی که ارسال ایمیل را انجام می‌دهد می‌توانید این کار را کنید:

class SendWelcomeEmail{
    public function handle()
    {
        DB::afterCommit(function(){
            Mail::to($user)->send(new WelcomeEmail());
        });
    }    
}

حالا وقتی کاربر ایجاد شود و ایونت صدازده شود، listener متد afterCommit را صدا می‌زند که منطق ارسال ایمیل را در local cache قرار می‌دهد که فقط در صورت کامیت شدن تمام database transaction های باز اجرا می‌شود.

اما اگر موافق باشید، قرار دادن کد داخل closure ظاهر زیبایی به کد نمی‌دهد. به همین دلیل، لاراول روش دیگری را برای اطمینان از اینکه listener شما بعد از کامیت شدن transaction انجام می‌شود معرفی کرده. بیایید SendWelcomeEmail را در نظر بگیریم:

class SendWelcomeEmail{
    public $afterCommit = true;

    public function handle()
    {
        Mail::to($user)->send(new WelcomeEmail());
    }    
}

با استفاده از afterCommit$ ما می‌توانیم به لاراول بگوییم که متد ()handle را فقط بعد از کامیت شدن transactionهای باز اجرا کند. اگر هیچ transaction فعالی وجود نداشته باشد، کد همانند حالت معمولی فورا اجرا می‌شود.

سناریوی بعدی که در آن به کار شما می‌آید dispatch کردن queued job, mail, notification, broadcasted event یا listener از داخل transaction می‌باشد. workerها ممکن است یک work را قبل از کامیت شدن transaction انتخاب کنند و کد در دیتابیس اجرا می‌شود در حالی که رکوردهای ویراش شده توسط transaction هنوز در حالت قدیم خود هستند. برای مثال:

DB::transaction(function(){
    $user = User::create([...]);

    SendWelcomeEmail::dispatch($user);

    Team::create([
        'owner_id' => $user->id,
        ...
    ]);
});

جاب SendWelcomeEmail قبل از اینکه transaction کامیت شود dispatch می‌شود. اگر worker فورا آن‌را انتخاب کند exception (استثنای) ModelNotFound اتفاق خواهد افتاد. چون کاربر فرستاده شده به جاب در دیتابیس ذخیره نشده است.

تنظیم کردن پراپرتی afterCommit$ روی true در کلاس job مورد نظر این اطمینان را به شما می‌دهد که job تنها بعد از اینکه transaction کامیت شد dispatch می‌شود. شما همچنین می‌تواند after_commit را در فایل تنظیمات صف‌ها در queue.php برابر true قرار بدهید:

'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    // ...
    'after_commit' => true
],

حالا تمام jobهای dispatch شده از طریق redis منتظر خواهند ماند تا تمام transactionهای باز کامیت شوند.

به عنوان جایگزین، شما میتوانید در مورد رفتار job هنگام dispatch کردن آن تصمیم بگیرید:

SendWelcomeEmail::dispatch($user)->afterCommit();
// OR
SendWelcomeEmail::dispatch($user)->beforeCommit();

شما می‌توانید از پراپرتی afterCommit$ در موارد زیر استفاده کنید:
mailables, notifications, jobs, listeners, model observers, and broadcasted events

منبع