तुम्ही ऑर्डर सेव्ह केली, पण ऑर्डर आयटम्स डेटाबेसमध्ये कधीच पोहोचले नाहीत. किंवा कदाचित स्टॉक काउंटर कमी झाला, पेमेंट गेटवे टाइमआउट झाला आणि आता ग्राहकाच्या कार्डवरून पैसे कापले गेले आहेत पण ऑर्डरचा कोणताही रेकॉर्ड नाही. हे प्रसंग सुरुवातीला किरकोळ वाटू शकतात, पण प्रत्यक्षात ते असे नसतात. एका व्यस्त ॲप्लिकेशनमध्ये, हे दररोजचे डोकेदुखीचे कारण बनतात.

याचे मूळ कारण जवळजवळ नेहमी एकच असते: डेटाबेसवरील लेखनांची (writes) अशी मालिका ज्यांना स्वतंत्र आणि विलग घटना मानले गेले. जेव्हा एक प्रक्रिया अयशस्वी होते, तेव्हा इतर प्रक्रिया मागे राहतात. याचे निराकरण म्हणजे 'डेटाबेस ट्रान्झॅक्शन' (database transaction), आणि Laravel मध्ये यासाठी DB::transaction() हे साधन उपलब्ध आहे.

ट्रान्झॅक्शन नेमके काय आश्वासन देते

डेटाबेस ट्रान्झॅक्शन अनेक ऑपरेशन्सना कामाच्या एका सिंगल युनिटमध्ये बांधते. डेटाबेस इंजिन हे सुनिश्चित करते की त्यातील सर्व गोष्टी एकतर कायमस्वरूपी 'कमिट' (commit) होतील किंवा पूर्णपणे 'रोलबॅक' (rollback) होतील. यामध्ये कोणताही मध्यमार्ग नसतो.

बँक ट्रान्सफरचा विचार करा. सिस्टिमने एका खात्यातून पैसे वजा करणे आणि दुसऱ्या खात्यात जमा करणे आवश्यक आहे. जर डेबिट झाल्यानंतर क्रेडिट प्रक्रिया अयशस्वी झाली, तर पैसे हवेत गायब होत नाहीत. बँक डेबिटची प्रक्रिया उलटवते. ही उलट करण्याची प्रक्रिया म्हणजे 'रोलबॅक' (rollback) होय. जर दोन्ही पायऱ्या यशस्वी झाल्या, तर ट्रान्सफर 'कमिट' (commit) होते, म्हणजेच नवीन शिल्लक रक्कम सुरक्षितपणे सेव्ह केली जाते.

ही 'सर्व किंवा काहीच नाही' (all-or-nothing) अशी पद्धत तुमचा डेटा सुसंगत (consistent) ठेवते. याशिवाय, अर्धवट अयशस्वी झालेल्या प्रक्रियांमुळे तुमच्या टेबल्समध्ये अर्धवट लिहिलेले रेकॉर्ड्स विखुरले जातात आणि ते रेकॉर्ड्स तुमच्या डेटाबेसमध्ये तसेच पडून राहतात, कारण कोणतीही ऑटोमेटेड प्रक्रिया त्यांना सुरक्षितपणे साफ कशी करायची हे जाणून नसते.

Laravel हे काम कसे करते

साध्या SQL मध्ये, तुम्हाला स्वतःला BEGIN, COMMIT, आणि ROLLBACK लिहावे लागतील आणि प्रत्येक संभाव्य त्रुटी (error) पकडण्याची काळजी घ्यावी लागेल, जेणेकरून ट्रान्झॅक्शन अर्धवट राहणार नाही. Laravel या सर्व क्लिष्ट गोष्टींना एका सिंगल मेथडमध्ये गुंडाळून देते.

तुम्ही 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',
    ]);
});

जर फॉरेन की (foreign key) नसल्यामुळे किंवा डेटाबेस कनेक्शन तुटल्यामुळे पेमेंट इन्सर्ट (payment insert) अयशस्वी झाले, तर ऑर्डर, ऑर्डर आयटम्स आणि स्टॉक मधील बदल हे सर्व रद्द केले जातात. यामुळे तुमची इन्व्हेंटरी विनाकारण कमी होत नाही आणि अशी कोणतीही ऑर्डर उरत नाही ज्यासाठी पेमेंट झाले नाही पण शिपमेंटची मागणी होत आहे.

हे तुम्हाला कुठे वाचवते

काही वर्कफ्लो पूर्णपणे ट्रान्झॅक्शनल सुरक्षिततेवर अवलंबून असतात. चेकआउटचे उदाहरण सर्वात स्पष्ट आहे, पण ही पद्धत सर्वत्र दिसून येते.

User registration. युजर रो (user row) तयार करणे, त्यानंतर प्रोफाइल रो आणि मग डिफॉल्ट सेटिंग्स. जर व्हॅलिडेशनच्या एखाद्या त्रुटीमुळे प्रोफाइल इन्सर्ट अयशस्वी झाले, तर प्रोफाइल नसलेला युजर एक 'घोस्ट अकाउंट' (ghost account) बनतो. ज्या पेजवर असे गृहीत धरले जाते की प्रत्येक युजरला प्रोफाइल आहे, ते पेज क्रॅश होईल किंवा खराब UI दाखवेल.

Bulk imports. ५० रेकॉर्ड्स इन्सर्ट करणारा CSV अपलोड, २६ व्या रो मध्ये चुकीची तारीख असल्यामुळे २५ रेकॉर्ड्स मागे सोडणार नाही. बॅचला ट्रान्झॅक्शनमध्ये गुंडाळल्यामुळे संपूर्ण इम्पोर्ट एक सिंगल युनिट म्हणून अयशस्वी होऊ शकतो. यामुळे ॲडमिनला तासनतास कोणते रेकॉर्ड्स अर्धवट आले आहेत हे शोधण्याऐवजी, फाईल दुरुस्त करून पुन्हा प्रयत्न करता येतो.

Inventory and accounting. जेव्हा एखादे टेबल भौतिक संसाधनांचा (physical resource) मागोवा घेते आणि दुसरे टेबल पैसे किंवा क्रेडिट्सचा मागोवा घेते, तेव्हा त्या दोन्ही गोष्टी एकत्र हलल्या पाहिजेत. त्यांना वेगळे केल्यास ऑडिटमध्ये तफावत निर्माण होऊ शकते आणि रिपोर्ट चुकीचे येऊ शकतात.

जेव्हा तुम्हाला मॅन्युअली हाताळण्याची गरज असते

Closure पद्धत बहुतेक प्रकरणांमध्ये काम करते, पण कधीकधी तुम्हाला अधिक नियंत्रणाची गरज असते. सर्विस क्लासमध्ये (service class) जटिल कंडिशनल लॉजिक किंवा रनटाइमला कमिट करायचे की नाही हे ठरवण्याची गरज असल्यास, सिंगल 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 ब्लॉक मधील क्रमाकडे लक्ष द्या: आधी रोलबॅक करा आणि मग थ्रो करा. जर तुम्ही रोलबॅक करण्यापूर्वी थ्रो केला, तर ट्रान्झॅक्शन कनेक्शनवर ओपन राहते. यामुळे रोक्स लॉक होऊ शकतात, इतर क्वेरीज डेडलॉक (deadlock) होऊ शकतात किंवा तुमचे कनेक्शन पूल संपू शकतात. मॅन्युअल ट्रान्झॅक्शन्स शक्तिशाली आहेत, पण ते साफसफाईची जबाबदारी तुमच्यावर टाकतात.

साईड इफेक्ट्स (Side Effects) ट्रान्झॅक्शनच्या बाहेर ठेवा

ही अशी नियम आहे जो प्रोडक्शनमध्ये टीम्सना अडचणीत आणतो. ट्रान्झॅक्शन फक्त डेटाबेसवरील कामे रद्द करू शकते. ते ईमेल पाठवणे थांबवू शकत नाही, क्लाउड स्टोरेजमधून फाईल डिलीट करू शकत नाही किंवा पेमेंट API द्वारे रिफंड करू शकत नाही.

जर तुम्ही ट्रान्झॅक्शन closure मध्ये Mail::send() कॉल केला आणि दोन ओळींनंतर डेटाबेस रोलबॅक झाला, तरीही तो ईमेल ग्राहकाच्या इनबॉक्समध्ये पोहोचतो. प्राप्तकर्त्याकडे आता अशा ऑर्डरचे इनव्हॉइस येते जी तुमच्या सिस्टिममध्ये अस्तित्वात नाही. हेच नियम Slack notifications, S3 वरील फाईल अपलोड किंवा webhook डिस्पॅचेससाठी देखील लागू होते.

योग्य क्रम असा आहे:

१. ट्रान्झॅक्शन पूर्ण करा आणि तुम्हाला आवश्यक असलेले कोणतेही IDs किंवा निकाल मिळवा. २. त्यानंतरच बाह्य 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 प्रतिसादासाठी तीन सेकंद थांबणे म्हणजे तीन सेकंदांचा अनावश्यक lock वेळ आहे. ट्रान्झॅक्शन संक्षिप्त आणि वेगवान ठेवा.

हा बग का लपून राहतो

सिंगल युजर आणि लोकल डेटाबेस असलेल्या डेव्हलपमेंट मशीनवर, संबंधित inserts जवळजवळ नेहमीच यशस्वी होतात. नेटवर्क स्थिर असते, डिस्क कधीही भरत नाही आणि कोणताही स्पर्धात्मक लोड नसतो. कोड बरोबर वाटतो कारण तो सहसा काम करतो.

प्रोडक्शन वेगळे असते. दोन ग्राहक अगदी एकाच मिलिसेकंदात ऑर्डर्स सबमिट करतात. डिप्लॉयमेंट दरम्यान एखादा queue worker कामाच्या मध्यंतरी रीस्टार्ट होतो. पेमेंट प्रोव्हायडर तीस सेकंदांसाठी time out होतो. ट्रान्झॅक्शनशिवाय, या घटनांमुळे orphan records आणि विसंगत एकूण बेरीज (mismatched totals) तयार होतात, ज्यांचा मागोवा घेणे त्रासदायक असते. सर्वात वाईट गोष्ट म्हणजे, स्टँडर्ड feature tests क्वचितच त्यांना पकडू शकतात, कारण ही त्रुटी वेळेवर अवलंबून असते आणि पायाभूत सुविधांशी (infrastructure) संबंधित असते, साध्या logic errors शी नाही.

ही तुमची सवय बनवा

DB::transaction() गुंतागुंत वाढवत नाही. ते नंतर अर्धवट डेटा साफ करण्याचा प्रयत्न करण्यातील सुप्त गुंतागुंत दूर करते. जर डेटाबेस ऑपरेशन्सचा समूह एकत्र असेल, तर सुरुवातीपासूनच त्यांच्याशी तसाच व्यवहार करा. तुमचा ॲप्लिकेशन लोड असतानाही अचूक राहील, तुमचे error logs स्वच्छ राहतील आणि तुमचा डेटाबेस अर्धवट राहिलेल्या रेकॉर्ड्सचा स्मशानभूमी बनणार नाही.