நீங்கள் ஆர்டரைச் சேமித்துவிட்டீர்கள், ஆனால் ஆர்டர் உருப்படிகள் (order items) தரவுத்தளத்திற்கு (database) சென்றபாடில்லை. அல்லது ஒருவேளை ஸ்டாக் கவுண்டர் குறைந்துவிட்டது, பேமெண்ட் கேட்வே டைம்அவுட் (timeout) ஆகிவிட்டது, இப்போது வாடிக்கையாளரின் கார்டில் பணம் எடுக்கப்பட்டுவிட்டது ஆனால் ஆர்டர் பதிவு இல்லை. இத்தகைய சூழல்கள் சாதாரணமானவை போலத் தோன்றலாம், ஆனால் அவை அவ்வாறு இல்லை. ஒரு பரபரப்பான பயன்பாட்டில் (busy application), இவை தினசரி தலைவலியாக மாறுகின்றன.

இதற்குக் காரணம் பெரும்பாலும் ஒன்றுதான்: தனித்தனி நிகழ்வுகளாகக் கருதப்பட்ட தொடர்ச்சியான தரவுத்தளப் பதிவுகள் (database writes). ஒன்று தோல்வியடையும் போது, மற்றவை பின்னால் தங்கிவிடுகின்றன. இதற்கான தீர்வு ஒரு தரவுத்தளப் பரிவர்த்தனை (database transaction), மற்றும் Laravel-இல், அதற்கான கருவி DB::transaction().

ஒரு பரிவர்த்தனை (Transaction) உண்மையில் என்ன உறுதியளிக்கிறது

ஒரு தரவுத்தளப் பரிவர்த்தனை பல செயல்பாடுகளை ஒரு ஒற்றை வேலைப் பிரிவாக (single unit of work) இணைக்கிறது. உள்ளே இருக்கும் அனைத்தும் நிரந்தரமாகச் சேமிக்கப்படும் (commit) அல்லது முழுமையாகத் திரும்பப் பெறப்படும் (rollback) என்று தரவுத்தள இயந்திரம் (database engine) உத்தரவாதம் அளிக்கிறது. இதில் இடைப்பட்ட நிலை என்று எதுவும் இல்லை.

ஒரு வங்கிப் பரிமாற்றத்தை (bank transfer) நினைத்துப் பாருங்கள். கணக்கிலிருந்து பணத்தை எடுத்து (debit), மற்றொரு கணக்கில் சேர்க்க (credit) வேண்டும். டெபிட் செய்த பிறகு கிரெடிட் செய்வதில் தோல்வி ஏற்பட்டால், பணம் அப்படியே காணாமல் போய்விடாது. வங்கி அந்த டெபிட் நடவடிக்கையைத் திரும்பப் பெறும். அந்தத் திரும்பப் பெறுதலே 'rollback' ஆகும். இரண்டு நிலைகளும் வெற்றியடைந்தால், பரிமாற்றம் 'commit' செய்யப்படுகிறது, அதாவது புதிய இருப்புத் தொகைகள் (balances) பாதுகாப்பாகச் சேமிக்கப்படுகின்றன.

இந்த 'அனைத்தும் அல்லது எதுவுமில்லை' (all-or-nothing) என்ற செயல்பாடே உங்கள் தரவைச் சீராக (consistent) வைத்திருக்கிறது. இது இல்லையென்றால், பாதி முடிந்த பதிவுகள் உங்கள் அட்டவணைகளில் (tables) சிதறிக்கிடக்கும், மேலும் எந்தத் தானியங்கி செயல்முறையும் அவற்றை எவ்வாறு பாதுகாப்பாகச் சுத்தம் செய்வது என்று தெரியாததால், அந்தப் பதிவுகள் உங்கள் தரவுத்தளத்தில் பயனற்றதாகிவிடும்.

Laravel இதை எவ்வாறு செய்கிறது

சாதாரண SQL-இல், நீங்கள் BEGIN, COMMIT, மற்றும் ROLLBACK ஆகியவற்றை நீங்களே எழுத வேண்டும், மேலும் எந்தத் தவறும் விடுபடாமல் இருக்க ஒவ்வொரு சாத்தியமான பிழையையும் (error) கையாள்வதை (catch) நினைவில் கொள்ள வேண்டும். Laravel அந்தச் சிக்கலான வேலைகளை (boilerplate) ஒரு ஒற்றை மெத்தடாக (method) சுருக்குகிறது.

நீங்கள் DB::transaction()-க்கு ஒரு closure-ஐ அனுப்புகிறீர்கள். Laravel பரிவர்த்தனையைத் தொடங்கி, உங்கள் குறியீட்டை (code) இயக்கும், மற்றும் அந்த closure எந்தத் தவறும் (exception) இல்லாமல் முடிந்தால், அது தானாகவே commit செய்துவிடும். ஏதேனும் தோல்வியடைந்தால், Laravel அந்த exception-ஐப் பிடித்து, அனைத்தையும் rollback செய்து, உங்கள் லாகிங் (logging) மற்றும் பிழை கையாளுதல் (error handling) எதிர்பார்த்தபடி செயல்படுவதற்காக அந்தப் பிழையை மீண்டும் வெளியிடும் (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 விடுபட்டதாலோ அல்லது தரவுத்தள இணைப்பு துண்டிக்கப்பட்டதாலோ பேமெண்ட் பதிவு தோல்வியடைந்தால், ஆர்டர், ஆர்டர் உருப்படிகள் மற்றும் ஸ்டாக் மாற்றங்கள் அனைத்தும் ரத்து செய்யப்படும். காரணமே இல்லாமல் ஸ்டாக் குறைந்துவிட்ட நிலை மற்றும் பணம் செலுத்தப்படாத ஒரு ஆர்டருக்காக ஷிப்மென்ட் (shipment) செய்ய வேண்டிய நிலை உங்களுக்கு ஏற்படாது.

இது உங்களுக்கு எங்கே உதவுகிறது

சில பணிப்பாய்வுகள் (workflows) முற்றிலும் பரிவர்த்தனை பாதுகாப்பைச் சார்ந்துள்ளன. செக்அவுட் (checkout) உதாரணம் மிகவும் வெளிப்படையானது, ஆனால் இந்த முறை எல்லா இடங்களிலும் காணப்படுகிறது.

பயனர் பதிவு (User registration). ஒரு பயனர் வரிசையை (user row) உருவாக்கி, பின்னர் ஒரு சுயவிவர வரிசையை (profile row), பிறகு இயல்பு அமைப்புகளை (default settings) உருவாக்குதல். ஒரு validation சிக்கலால் சுயவிவரப் பதிவு தோல்வியடைந்தால், சுயவிவரம் இல்லாத பயனர் ஒரு 'ghost account'-ஆக மாறிவிடுவார். ஒவ்வொரு பயனருக்கும் ஒரு சுயவிவரம் இருக்கும் என்று கருதும் எந்தப் பக்கமும் செயலிழக்கலாம் அல்லது சிதைந்த UI-ஐக் காட்டலாம்.

மொத்த இறக்குமதிகள் (Bulk imports). ஐம்பது பதிவுகளைச் சேர்க்கும் ஒரு CSV upload, இருபத்தைந்து பதிவுகளை மட்டும் விட்டுவிட்டுப் போகக்கூடாது; ஏனெனில் இருபத்தி ஆறாவது வரிசையில் தேதி தவறாக இருக்கலாம். ஒரு தொகுப்பை (batch) பரிவர்த்தனைக்குள் வைப்பதன் மூலம், முழு இறக்குமதியும் ஒரே அலகாகத் தோல்வியடையச் செய்யலாம். நிர்வாகி (admin) எந்தெந்த வரிகள் பாதிப் பதிவாகிவிட்டன என்று தேடி நேரத்தைச் செலவிடுவதைத் தவிர்த்துவிட்டு, கோப்பைச் சரிசெய்து மீண்டும் முயற்சி செய்யலாம்.

சரக்கு மற்றும் கணக்கியல் (Inventory and accounting). ஒரு அட்டவணை ஒரு இயற்பியல் வளத்தைக் (physical resource) கண்காணிக்கும் போதும், மற்றொரு அட்டவணை பணம் அல்லது வரவுத் தொகையைக் கண்காணிக்கும் போதும், இரண்டும் இணைந்து செயல்பட வேண்டும். அவற்றைத் தனித்தனியாகப் பிரிப்பது, கணக்கீடுகள் பொருந்தாத (reconcile) தணிக்கைகளுக்கும் (audits), தவறான அறிக்கைகளுக்கும் வழிவகுக்கும்.

எப்போது நீங்கள் கைமுறையாக (Manual) இயக்க வேண்டும்

Closure அணுகுமுறை பெரும்பாலான சந்தர்ப்பங்களைக் கையாள்கிறது, ஆனால் சில நேரங்களில் உங்களுக்கு அதிகக் கட்டுப்பாடு தேவைப்படலாம். ஒரு service class-க்குள் சிக்கலான நிபந்தனைத் தர்க்கங்கள் (conditional logic) இருப்பது அல்லது runtime-இல் commit செய்ய வேண்டுமா என்பதைத் தீர்மானிக்க வேண்டிய தேவை ஆகியவை ஒரு ஒற்றை 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 block-க்குள் உள்ள வரிசையைக் கவனியுங்கள்: முதலில் rollback செய்யுங்கள், பின்னர் throw செய்யுங்கள். நீங்கள் rollback செய்வதற்கு முன்பே throw செய்தால், பரிவர்த்தனை இணைப்பில் (connection) திறந்தே இருக்கும். அது வரிசைகளைத் (rows) பூட்டிவிடலாம் (lock), பிற வினவல்களை (queries) முடக்கிவிடலாம் (deadlock), அல்லது உங்கள் connection pool-ஐத் தீர்த்துவிடலாம். கைமுறைப் பரிவர்த்தனைகள் சக்திவாய்ந்தவை, ஆனால் அவற்றின் சுத்திகரிப்புப் பொறுப்பு (cleanup burden) உங்களைச் சாரும்.

பக்க விளைவுகளை (Side Effects) பரிவர்த்தனைக்கு வெளியே வைத்திருங்கள்

இதுதான் தயாரிப்புச் சூழலில் (production) குழுக்களைப் பாதிக்கும் விதி. ஒரு பரிவர்த்தனையால் தரவுத்தளப் பணிகளை மட்டுமே ரத்து செய்ய முடியும். அது ஒரு மின்னஞ்சலைத் திரும்ப அனுப்ப முடியாது, கிளவுட் ஸ்டோரேஜில் (cloud storage) இருந்து ஒரு கோப்பை நீக்க முடியாது அல்லது பேமெண்ட் API மூலம் பணத்தைத் திரும்பப் பெற முடியாது.

நீங்கள் transaction closure-க்குள் ஒரு Mail::send() அழைப்பை வைத்தால், மற்றும் இரண்டு வரிகளுக்குப் பிறகு தரவுத்தளம் rollback செய்யப்பட்டால், அந்த மின்னஞ்சல் வாடிக்கையாளரின் இன்பாக்ஸிற்குச் சென்றுவிடும். உங்கள் அமைப்பில் இல்லாத ஒரு ஆர்டருக்கான விலைப்பட்டியல் (invoice) இப்போது பெறுநரிடம் இருக்கும். Slack அறிவிப்புகள், S3-க்கு கோப்புகளைப் பதிவேற்றுதல் அல்லது webhook அனுப்புதல் ஆகியவற்றிற்கும் இதுவே பொருந்தும்.

சரியான வரிசைமுறை இதுதான்:

  1. பரிவர்த்தனையை (transaction) முடித்துவிட்டு, உங்களுக்குத் தேவையான ID-கள் அல்லது முடிவுகளைப் பெற்றுக்கொள்ளுங்கள்.
  2. அதன் பின்னரே வெளிப்புறத் தாக்கங்களை (external side effects) தூண்டவும்.

உதாரணமாக, உறுதிப்படுத்தல் மின்னஞ்சலை (confirmation email) commit செய்த பிறகு வரிசையில் (queue) சேர்க்கவும், அதற்குள் அல்ல:

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

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

வெளிப்புற அழைப்புகளை (external calls) பரிவர்த்தனைக்கு வெளியே வைத்திருக்க இரண்டாவது காரணம்: நேரம். ஒரு பரிவர்த்தனை லாக்ஸ்களை (locks) பிடித்துக் கொள்ளும் மற்றும் ஒரு தரவுத்தள இணைப்பை (database connection) பிஸியாக வைத்திருக்கும். ஒரு பரிவர்த்தனைக்கு உள்ளே இருக்கும்போது Stripe API பதிலுக்காக மூன்று வினாடிகள் காத்திருப்பது என்பது, மூன்று வினாடிகள் தேவையற்ற லாக் நேரமாகும். பரிவர்த்தனையைச் சுருக்கமாகவும் வேகமாகவும் வைத்திருங்கள்.

இந்த பிழை ஏன் மறைந்திருக்கிறது

ஒரு பயனர் மற்றும் உள்ளூர் தரவுத்தளம் (local database) கொண்ட மேம்பாட்டு இயந்திரத்தில் (development machine), தொடர்புடைய இன்செர்ட்கள் (inserts) எப்போதும் வெற்றிகரமாகவே முடியும். நெட்வொர்க் நிலையானது, டிஸ்க் de never fills, மற்றும் போட்டியிடும் சுமை (competing load) எதுவும் இருக்காது. குறியீடு (code) பொதுவாகச் சரியாகச் செயல்படுவதால், அது சரியாகத் தோன்றுகிறது.

தயாரிப்புச் சூழல் (Production) வேறுபட்டது. இரண்டு வாடிக்கையாளர்கள் ஒரே மில்லிசெகண்டில் ஆர்டர்களைச் சமர்ப்பிக்கிறார்கள். ஒரு deployment-ன் போது ஒரு queue worker வேலையின் நடுவே மீண்டும் தொடங்குகிறது. ஒரு கட்டண வழங்குநர் (payment provider) முப்பது வினாடிகளுக்கு timeout ஆகிறார். பரிவர்த்தனைகள் இல்லையென்றால், இந்த நிகழ்வுகள் கண்டறிவதற்கு கடினமானத் தனித்த பதிவுகளையும் (orphan records) பொருந்தாத மொத்தத் தொகைகளையும் உருவாக்கும். மிக மோசமான விஷயம் என்னவென்றால், நிலையான அம்சம் சோதனைகள் (feature tests) இவற்றை அரிதாகவே கண்டறியும், ஏனெனில் இந்தத் தோல்வி முறை நேரத்தைச் சார்ந்தது மற்றும் உள்கட்டமைப்போடு (infrastructure) தொடர்புடையது, எளிய தர்க்கப் பிழைகள் (logic errors) அல்ல.

இதற்கான தீர்வு விசித்திரமானது அல்ல. இது ஒரு இயந்திரவியல் முறை. இரண்டு அல்லது அதற்கு மேற்பட்ட தொடர்புடைய எழுத்துக்களை (writes) நீங்கள் கண்டால், அவற்றை ஒரு பரிவர்த்தனைக்குள் கொண்டு வாருங்கள் (wrap). காலப்போக்கில், இது ஒரு கோரிக்கையை (request) சரிபார்ப்பது போலவே தானியங்கித் தன்மையைப் பெறும்.

இதை ஒரு இயல்பான பழக்கமாக்குங்கள்

DB::transaction() சிக்கலைச் சேர்க்காது. மாறாக, ஒரு செயல் முடிந்த பிறகு பகுதித் தரவுகளை (partial data) சுத்தம் செய்ய முயற்சிக்கும் மறைமுகச் சிக்கலை இது நீக்குகிறது. தரவுத்தளச் செயல்பாடுகளின் ஒரு குழு ஒன்றாகச் சேர வேண்டியிருந்தால், ஆரம்பத்திலிருந்தே அவற்றை அவ்வாறே கையாளவும். உங்கள் பயன்பாடு (application) அதிக சுமையின் போதும் சரியாகச் செயல்படும், உங்கள் பிழைப் பதிவுகள் (error logs) சுத்தமாக இருக்கும், மேலும் உங்கள் தரவுத்தளம் பாதியிலேயே முடிக்கப்பட்ட பதிவுகளின் மயானமாக மாறாது.