آپ نے آرڈر تو محفوظ کر لیا، لیکن آرڈر کی اشیاء (order items) ڈیٹا بیس تک کبھی پہنچ ہی نہیں پائیں۔ یا شاید اسٹاک کا کاؤنٹر کم ہو گیا، پیمنٹ گیٹ وے ٹائم آؤٹ ہو گیا، اور اب کسٹمر کے کارڈ سے پیسے کٹ چکے ہیں لیکن آرڈر کا کوئی ریکارڈ موجود نہیں۔ یہ منظرنامے تب تک معمولی لگتے ہیں جب تک کہ ایسا ہو نہ جائے۔ ایک مصروف ایپلی کیشن میں، یہ روزانہ کے سردرد بن جاتے ہیں۔

اس کی اصل وجہ تقریباً ہمیشہ ایک ہی ہوتی ہے: ڈیٹا بیس رائٹس (writes) کا ایک ایسا سلسلہ جسے الگ الگ، آزادانہ واقعات سمجھا گیا۔ جب ایک ناکام ہوتا ہے، تو باقی پیچھے رہ جاتے ہیں۔ اس کا حل ڈیٹا بیس ٹرانزیکشن (database transaction) ہے، اور Laravel میں اس کے لیے DB::transaction() کا ٹول استعمال ہوتا ہے۔

ٹرانزیکشن اصل میں کیا وعدہ کرتی ہے

ایک ڈیٹا بیس ٹرانزیکشن متعدد آپریشنز کو کام کے ایک واحد یونٹ (single unit of work) میں باندھ دیتی ہے۔ ڈیٹا بیس انجن اس بات کی ضمانت دیتا ہے کہ اندر موجود تمام چیزیں یا تو مستقل طور پر کمٹ (commit) ہوں گی یا مکمل طور پر رول بیک (rollback) ہو جائیں گی۔ اس میں کوئی درمیانی راستہ نہیں ہوتا ہے۔

ایک بینک ٹرانسفر کے بارے میں سوچیں۔ سسٹم کو ایک اکاؤنٹ سے رقم منہا (debit) کرنی چاہیے اور دوسرے میں جمع (credit) کرنی چاہیے۔ اگر ڈیبٹ کے بعد کریڈٹ ناکام ہو جائے، تو رقم محض غائب نہیں ہو جاتی۔ بینک ڈیبٹ کو واپس لے لیتا ہے۔ وہ واپسی ہی رول بیک (rollback) ہے۔ اگر دونوں مراحل کامیاب ہو جائیں، تو ٹرانسفر کمٹ ہو جاتا ہے، جس کا مطلب ہے کہ نئے بیلنس مستقل طور پر محفوظ ہو گئے ہیں۔

یہ "سب کچھ یا کچھ بھی نہیں" (all-or-nothing) والا رویہ ہی آپ کے ڈیٹا کو مستقل (consistent) رکھتا ہے۔ اس کے بغیر، جزوی ناکامیاں آپ کے ٹیبلز میں ادھورے لکھے ہوئے ریکارڈز بکھیر دیتی ہیں، اور وہ ریکارڈز آپ کے ڈیٹا بیس میں پڑے رہتے ہیں کیونکہ کوئی خودکار عمل یہ نہیں جانتا کہ انہیں محفوظ طریقے سے کیسے صاف کیا جائے۔

Laravel اس کام کو کیسے انجام دیتا ہے

سادہ SQL میں، آپ خود BEGIN، COMMIT اور ROLLBACK لکھیں گے، اور ہر ممکنہ غلطی (error) کو پکڑنے کا خیال رکھیں گے تاکہ کوئی ٹرانزیکشن ادھوری نہ رہ جائے۔ Laravel اس تمام بوائلر پلیٹ (boilerplate) کوڈ کو ایک ہی میتھڈ میں لپیٹ دیتا ہے۔

آپ DB::transaction() کو ایک closure پاس کرتے ہیں۔ Laravel ٹرانزیکشن شروع کرتا ہے، آپ کا کوڈ چلاتا ہے، اور اگر closure بغیر کسی exception کے مکمل ہو جائے، تو یہ خود بخود اسے کمٹ کر دیتا ہے۔ اگر کچھ بھی ناکام ہوتا ہے، تو Laravel exception کو پکڑ لیتا ہے، سب کچھ رول بیک کر دیتا ہے، اور ایرر کو دوبارہ تھرو (rethrow) کر دیتا ہے تاکہ آپ کی لاگنگ اور ایرر ہینڈلنگ توقع کے مطابق کام کرتی رہے۔

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    $order = Order::create([/* ... */]);
    
    foreach ($cart->items as $item) {
        OrderItem::create([
            'order_id' => $order->id,
            'product_id' => $item->product_id,
            'quantity' => $item->quantity,
        ]);
        
        Product::find($item->product_id)
               ->decrement('stock', $item->quantity);
    }
    
    Payment::create([
        'order_id' => $order->id,
        'amount' => $cart->total,
        'status' => 'completed',
    ]);
});

اگر پیمنٹ کا اندراج (insert) اس لیے ناکام ہو جاتا ہے کیونکہ کوئی foreign key غائب ہے یا ڈیٹا بیس کنکشن ٹوٹ جاتا ہے، تو آرڈر، آرڈر کی اشیاء، اور اسٹاک کی تبدیلیاں سب منسوخ ہو جاتی ہیں۔ آپ کے پاس ایسا انوینٹری نہیں بچتا جو بلاوجہ غائب ہو گیا ہو، اور نہ ہی آپ کے پاس ایسا آرڈر بچتا ہے جو شپمنٹ کا مطالبہ کر رہا ہو لیکن اس کی ادائیگی نہ ہوئی ہو۔

یہ آپ کو کہاں بچاتا ہے

کچھ ورک فلو (workflows) مکمل طور پر ٹرانزیکشنل سیفٹی پر منحصر ہوتے ہیں۔ چیک آؤٹ کی مثال سب سے واضح ہے، لیکن یہ پیٹرن ہر جگہ نظر آتا ہے۔

User registration۔ ایک یوزر رو (user row) بنانا، پھر پروفائل رو، اور پھر ڈیفالٹ سیٹنگز۔ اگر پروفائل کا اندراج کسی ویلیڈیشن کی وجہ سے ناکام ہو جاتا ہے، تو پروفائل کے بغیر یوزر ایک "گھوسٹ اکاؤنٹ" بن جاتا ہے۔ کوئی بھی پیج جو یہ فرض کرتا ہے کہ ہر یوزر کا پروفائل ہے، کریش ہو جائے گا یا خراب UI دکھائے گا۔

Bulk imports۔ ایک CSV اپ لوڈ جو پچاس ریکارڈز درج کرتا ہے، اسے پچیس پر نہیں رکنا چاہیے صرف اس لیے کہ چھبیسویں رو میں تاریخ غلط تھی۔ پورے بیچ (batch) کو ایک ٹرانزیکشن میں لپیٹنے سے پورا امپورٹ ایک صاف ستھرے یونٹ کے طور پر ناکام ہو جاتا ہے۔ ایڈمن فائل کو درست کرتا ہے اور دوبارہ کوشش کرتا ہے، بجائے اس کے کہ گھنٹوں یہ ڈھونڈنے میں گزار دے کہ کون سے ریکارڈز جزوی طور پر لیک ہو گئے۔

Inventory and accounting۔ جب بھی ایک ٹیبل کسی جسمانی وسیلے (physical resource) کو ٹریک کرتا ہے اور دوسرا ٹیبل رقم یا کریڈٹ کو ٹریک کرتا ہے، تو ان دونوں کا ایک ساتھ حرکت کرنا ضروری ہے۔ انہیں الگ کرنے سے ایسے آڈٹ کی دعوت ملتی ہے جو میچ نہیں کرتے اور ایسی رپورٹس ملتی ہیں جو غلط ہوتی ہیں۔

جب آپ کو دستی طور پر (Manually) کنٹرول کرنے کی ضرورت ہو

Closure والا طریقہ زیادہ تر کیسز کو کور کر لیتا ہے، لیکن کبھی کبھی آپ کو زیادہ کنٹرول کی ضرورت ہوتی ہے۔ سروس کلاس کے اندر پیچیدہ کنڈیشنل لاجک، یا رن ٹائم پر یہ فیصلہ کرنے کی ضرورت کہ کمٹ کرنا ہے یا نہیں، ایک سنگل closure کو مشکل بنا سکتی ہے۔ ان لمحات میں، آپ ٹرانزیکشن کو خود مینیج کر سکتے ہیں:

DB::beginTransaction();

try {
    // Run your operations
    $order = Order::create([/* ... */]);
    // ... more work ...
    
    if ($someBusinessRulePasses) {
        DB::commit();
    } else {
        DB::rollBack();
    }
} catch (\Throwable $e) {
    DB::rollBack();
    throw $e;
}

catch بلاک کے اندر ترتیب پر غور کریں: پہلے رول بیک کریں، پھر تھرو کریں۔ اگر آپ رول بیک کرنے سے پہلے تھرو کر دیتے ہیں، تو ٹرانزیکشن کنکشن پر کھلی رہ جاتی ہے۔ اس سے روز (rows) لاک ہو سکتے ہیں، دوسری کوئریز ڈیڈ لاک (deadlock) ہو سکتی ہیں، یا آپ کا کنکشن پول ختم ہو سکتا ہے۔ دستی ٹرانزیکشنز طاقتور ہوتی ہیں، لیکن وہ صفائی کا بوجھ آپ پر ڈال دیتی ہیں۔

سائیڈ ایفیکٹس (Side Effects) کو ٹرانزیکشن سے دور رکھیں

یہ وہ اصول ہے جو پروڈکشن میں ٹیموں کو مشکل میں ڈال دیتا ہے۔ ایک ٹرانزیکشن صرف ڈیٹا بیس کے کام کو منسوخ کر سکتی ہے۔ یہ ای میل واپس نہیں بھیج سکتی، کلاؤڈ اسٹوریج سے فائل ڈیلیٹ نہیں کر سکتی، یا پیمنٹ API کے ذریعے چارج واپس (refund) نہیں کر سکتی۔

اگر آپ ٹرانزیکشن closure کے اندر Mail::send() کال کرتے ہیں، اور دو لائنوں بعد ڈیٹا بیس رول بیک ہو جاتا ہے، تو وہ ای میل پھر بھی کسٹمر کے ان باکس میں پہنچ جاتی ہے۔ وصول کنندہ کے پاس اب ایک ایسے آرڈر کا انوائس ہوتا ہے جو آپ کے سسٹم میں موجود ہی نہیں ہے۔ یہی بات Slack نوٹیفیکیشنز، S3 پر فائل اپ لوڈز، یا ویب ہک ڈسپیکچز پر بھی لاگو ہوتی ہے۔

صحیح ترتیب یہ ہے:

  1. ٹرانزیکشن مکمل کریں اور وہ تمام IDs یا نتائج حاصل کریں جن کی آپ کو ضرورت ہے۔
  2. اس کے بعد ہی بیرونی سائیڈ ایفیکٹس (side effects) کو ٹرگر کریں۔

مثال کے طور پر، کنفرمیشن ای میل کو کمٹ (commit) کے بعد کیو (queue) میں ڈالیں، اس کے اندر نہیں۔

$order = DB::transaction(function () {
    // database work only
    return Order::create([/* ... */]);
});

// Side effects happen after the database state is solid
OrderConfirmationJob::dispatch($order);

بیرونی کالز کو ٹرانزیکشن سے باہر رکھنے کی دوسری وجہ وقت ہے۔ ایک ٹرانزیکشن لاکس (locks) برقرار رکھتی ہے اور ڈیٹا بیس کنکشن کو مصروف رکھتی ہے۔ ٹرانزیکشن کے دوران Stripe API کے جواب کے لیے تین سیکنڈ انتظار کرنا، لاک کے تین غیر ضروری سیکنڈز کا ضیاع ہے۔ ٹرانزیکشن کو مختصر اور تیز رکھیں۔

یہ بگ (bug) کیوں چھپا رہتا ہے

ایک ڈویلپمنٹ مشین پر جہاں صرف ایک صارف اور لوکل ڈیٹا بیس ہو، تو متعلقہ انسرٹس (inserts) تقریباً ہمیشہ کامیاب ہو جاتے ہیں۔ نیٹ ورک مستحکم ہوتا ہے، ڈسک کبھی بھرتی نہیں ہے، اور کوئی مقابلہ کرنے والا لوڈ (load) نہیں ہوتا۔ کوڈ درست نظر آتا ہے کیونکہ یہ عام طور پر کام کرتا ہے۔

پروڈکشن (Production) مختلف ہوتی ہے۔ دو صارفین بالکل ایک ہی ملی سیکنڈ میں آرڈر جمع کرواتے ہیں۔ ڈیپلائمنٹ کے دوران ایک کیو ورکر (queue worker) کام کے بیچ میں ہی ری اسٹارٹ ہو جاتا ہے۔ کوئی پیمنٹ پرووائیڈر تیس سیکنڈ کے لیے ٹائم آؤٹ ہو جاتا ہے۔ ٹرانزیکشنز کے بغیر، یہ واقعات یتیم ریکارڈز (orphan records) اور غلط ٹوٹل پیدا کرتے ہیں جن کا سراغ لگانا تکلیف دہ ہوتا ہے۔ سب سے برا حصہ یہ ہے کہ معیاری فیچر ٹیسٹ انہیں شاذ و نادر ہی پکڑ پاتے ہیں، کیونکہ ناکامی کا طریقہ کار وقت (timing) پر منحصر ہوتا ہے اور انفراسٹرکچر سے جڑا ہوتا ہے، نہ کہ سادہ منطقی غلطیوں (logic errors) سے۔

حل کوئی غیر معمولی چیز نہیں ہے۔ یہ میکانکی ہے۔ اگر آپ دو یا دو سے زیادہ متعلقہ رائٹس (writes) دیکھتے ہیں، تو انہیں (ٹرانزیکشن میں) لپیٹ دیں۔ وقت کے ساتھ، یہ ایک درخواست کو ویلیڈیٹ کرنے کی طرح خودکار ہو جانا چاہیے۔

اسے ایک عادت (reflex) بنا لیں

DB::transaction() پیچیدگی نہیں بڑھاتا۔ یہ بعد میں ادھورے ڈیٹا کو صاف کرنے کی کوشش کرنے والی چھپی ہوئی پیچیدگی کو ختم کرتا ہے۔ اگر ڈیٹا بیس کے آپریشنز کا ایک گروپ ایک ساتھ تعلق رکھتا ہے، تو شروع سے ہی ان کے ساتھ ایسا ہی سلوک کریں۔ آپ کی ایپلی کیشن لوڈ کے تحت درست رہے گی، آپ کے ایرر لاگز صاف رہیں گے، اور آپ کا ڈیٹا بیس ادھورے ریکارڈز کا قبرستان نہیں بنے گا۔