તમે ઓર્ડર સેવ કર્યો, પરંતુ ઓર્ડર આઈટમ્સ ક્યારેય ડેટાબેઝમાં પહોંચી નહીં. અથવા કદાચ સ્ટોક કાઉન્ટર ઘટી ગયું, પેમેન્ટ ગેટવે ટાઈમઆઉટ થઈ ગયું, અને હવે ગ્રાહકના કાર્ડમાંથી પૈસા કપાઈ ગયા છે પણ ઓર્ડરનો કોઈ રેકોર્ડ નથી. આ પરિસ્થિતિઓ શરૂઆતમાં સામાન્ય લાગે છે, પરંતુ તે ક્યારેય પણ મુશ્કેલી બની શકે છે. વ્યસ્ત એપ્લિકેશનમાં, તે રોજિંદા માથાકૂટ બની જાય છે.

તેનું મૂળ કારણ લગભગ હંમેશા સમાન હોય છે: ડેટાબેઝ રાઈટ્સ (writes) નો એક એવો ક્રમ જેને અલગ-અલગ, સ્વતંત્ર ઘટનાઓ તરીકે ગણવામાં આવ્યા હોય. જ્યારે એક નિષ્ફળ જાય છે, ત્યારે અન્ય પાછળ રહી જાય છે. તેનો ઉકેલ ડેટાબેઝ ટ્રાન્ઝેક્શન (database transaction) છે, અને Laravel માં, તેના માટેનું સાધન DB::transaction() છે.

ટ્રાન્ઝેક્શન ખરેખર શું વચન આપે છે

ડેટાબેઝ ટ્રાન્ઝેક્શન અનેક કામગીરીઓને કામના એક સિંગલ યુનિટમાં બાંધે છે. ડેટાબેઝ એન્જિન ખાતરી આપે છે કે અંદરની બધી જ બાબતો કાં તો કાયમી રીતે કમિટ (commit) થાય અથવા સંપૂર્ણપણે રોલબેક (rollback) થાય. વચ્ચેનો કોઈ રસ્તો નથી.

બેંક ટ્રાન્સફર વિશે વિચારો. સિસ્ટમે એક ખાતામાંથી પૈસા કાપવા (debit) જોઈએ અને બીજા ખાતામાં જમા (credit) કરવા જોઈએ. જો ડેબિટ થયા પછી ક્રેડિટ કરવામાં નિષ્ફળતા મળે, તો પૈસા માત્ર અદ્રશ્ય થઈ જતા નથી. બેંક ડેબિટની પ્રક્રિયા ઉલટાવી દે છે. તે રિવર્સલ એટલે જ રોલબેક છે. જો બંને સ્ટેપ્સ સફળ થાય, તો ટ્રાન્સફર કમિટ થાય છે, જેનો અર્થ છે કે નવા બેલેન્સ કાયમી રીતે સેવ થઈ ગયા છે.

આ 'ઓલ-ઓર-નથિંગ' (all-or-nothing) વર્તન જ તમારા ડેટાને સુસંગત (consistent) રાખે છે. તેના વગર, આંશિક નિષ્ફળતાઓને કારણે તમારા ટેબલ્સમાં અડધા લખાયેલા રેકોર્ડ્સ વિખરાઈ જાય છે, અને તે રેકોર્ડ્સ તમારા ડેટાબેઝમાં બગડી જાય છે કારણ કે કોઈ ઓટોમેટેડ પ્રોસેસને ખબર નથી હોતી કે તેમને સુરક્ષિત રીતે કેવી રીતે સાફ કરવા.

Laravel આ કેવી રીતે કરે છે

સાદા SQL માં, તમારે BEGIN, COMMIT, અને ROLLBACK જાતે લખવા પડે, અને દરેક સંભવિત ભૂલને પકડવાનું યાદ રાખવું પડે જેથી તમે ક્યારેય ટ્રાન્ઝેક્શન અધૂરું ન છોડો. Laravel આ બધી જ જટિલતાઓને એક સિંગલ મેથડમાં સમાવી દે છે.

તમે DB::transaction() ને એક closure પાસ કરો છો. Laravel ટ્રાન્ઝેક્શન શરૂ કરે છે, તમારો કોડ રન કરે છે, અને જો closure કોઈ એક્સેપ્શન (exception) ફેંક્યા વગર પૂરું થાય, તો તે આપમેળે કમિટ કરી દે છે. જો કંઈ પણ નિષ્ફળ જાય, તો Laravel એક્સેપ્શનને પકડી લે છે, બધું રોલબેક કરે છે, અને ભૂલને ફરીથી ફેંકે છે જેથી તમારું લોગિંગ અને એરર હેન્ડલિંગ અપેક્ષા મુજબ કામ કરે.

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) ખૂટતી હોય અથવા ડેટાબેઝ કનેક્શન તૂટી જાય અને પેમેન્ટ ઇન્સર્ટ નિષ્ફળ જાય, તો ઓર્ડર, ઓર્ડર આઈટમ્સ અને સ્ટોકમાં થયેલા ફેરફારો બધું જ રદ થઈ જાય છે. તમારી પાસે કારણ વગર ગાયબ થયેલો ઇન્વેન્ટરી સ્ટોક રહેશે નહીં, અને તમે એવા ઓર્ડર સાથે પણ નહીં રહો જેની શિપમેન્ટની માંગણી કરવામાં આવી હોય પણ જેનું પેમેન્ટ ક્યારેય થયું ન હોય.

આ તમને ક્યાં બચાવે છે

કેટલાક વર્કફ્લો સંપૂર્ણપણે ટ્રાન્ઝેક્શનલ સેફ્ટી પર આધારિત હોય છે. ચેકઆઉટનું ઉદાહરણ સૌથી સ્પષ્ટ છે, પરંતુ આ પેટર્ન બધે જ જોવા મળે છે.

User registration. યુઝર રો (row) બનાવવો, પછી પ્રોફાઇલ રો, અને પછી ડિફોલ્ટ સેટિંગ્સ. જો વેલિડેશનના કોઈ કારણસર પ્રોફાઇલ ઇન્સર્ટ નિષ્ફળ જાય, તો પ્રોફાઇલ વગરનો યુઝર એક 'ગોસ્ટ એકાઉન્ટ' બની જાય છે. કોઈપણ પેજ જે એવું માને છે કે દરેક યુઝર પાસે પ્રોફાઇલ છે, તે ક્રેશ થઈ શકે છે અથવા બ્રોકન UI બતાવી શકે છે.

Bulk imports. CSV અપલોડ જે પચાસ રેકોર્ડ્સ ઇન્સર્ટ કરે છે, તે પચાસમાંથી પચ્ચીસ રેકોર્ડ્સ પાછળ છોડી દેવા જોઈએ નહીં કારણ કે છવ્વીસમાં રેકોર્ડમાં તારીખ ખોટી હતી. આ બેચને ટ્રાન્ઝેક્શનમાં રાખવાથી આખી ઇમ્પોર્ટ એક સિંગલ યુનિટ તરીકે નિષ્ફળ જશે. એડમિન કલાકો સુધી કયા રેકોર્ડ્સ અધૂરા રહી ગયા તે શોધવાને બદલે ફાઇલ સુધારીને ફરીથી પ્રયાસ કરી શકે છે.

Inventory and accounting. જ્યારે પણ એક ટેબલ ભૌતિક સંસાધનને ટ્રેક કરે છે અને બીજું ટેબલ પૈસા અથવા ક્રેડિટને ટ્રેક કરે છે, ત્યારે બંને સાથે ચાલવા જોઈએ. તેમને અલગ કરવાથી એડિટ્સ (audits) માં તફાવત આવી શકે છે અને રિપોર્ટ્સ ખોટા પડી શકે છે.

જ્યારે તમારે મેન્યુઅલી કંટ્રોલ કરવાની જરૂર હોય

ક્લોઝર એપ્રોચ મોટાભાગના કિસ્સાઓમાં કામ કરે છે, પરંતુ ક્યારેક તમારે વધુ કંટ્રોલની જરૂર હોય છે. સર્વિસ ક્લાસની અંદર જટિલ કન્ડિશનલ લોજિક, અથવા રનટાઇમ પર કમિટ કરવું કે નહીં તે નક્કી કરવાની જરૂરિયાત, ક્લોઝરને અઘરું બનાવી શકે છે. તે ક્ષણોમાં, તમે ટ્રાન્ઝેક્શન જાતે મેનેજ કરી શકો છો:

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 બ્લોકની અંદરનો ક્રમ જુઓ: પહેલા રોલબેક કરો, પછી થ્રો (throw) કરો. જો તમે રોલબેક કરતા પહેલા થ્રો કરો છો, તો કનેક્શન પર ટ્રાન્ઝેક્શન ખુલ્લું રહી જાય છે. તેનાથી રો (rows) લોક થઈ શકે છે, અન્ય ક્વેરીઝ ડેડલોક (deadlock) થઈ શકે છે, અથવા તમારું કનેક્શન પૂલ ખતમ થઈ શકે છે. મેન્યુઅલ ટ્રાન્ઝેક્શન શક્તિશાળી છે, પરંતુ તે સફાઈનો બોજ તમારા પર મૂકે છે.

સાઇડ ઇફેક્ટ્સને ટ્રાન્ઝેક્શનથી દૂર રાખો

આ એવો નિયમ છે જે પ્રોડક્શનમાં ટીમોને મુશ્કેલીમાં મૂકી દે છે. ટ્રાન્ઝેક્શન ફક્ત ડેટાબેઝના કામને જ ઉલટાવી શકે છે. તે ઈમેલ અનસેન્ડ (unsend) કરી શકતું નથી, ક્લાઉડ સ્ટોરેજમાંથી ફાઇલ ડિલીટ કરી શકતું નથી, અથવા પેમેન્ટ API દ્વારા ચાર્જ રિફંડ કરી શકતું નથી.

જો તમે ટ્રાન્ઝેક્શન ક્લોઝરની અંદર Mail::send() કોલ મૂકો છો, અને બે લાઇન પછી ડેટાબેઝ રોલબેક થાય છે, તો તે ઈમેલ ગ્રાહકના ઇનબોક્સમાં પહોંચી જશે. પ્રાપ્તકર્તા પાસે હવે એવા ઓર્ડરનું ઇનવોઇસ હશે જે તમારી સિસ્ટમમાં અસ્તિત્વ ધરાવતું નથી. આ જ વાત Slack નોટિફિકેશન, S3 પર ફાઇલ અપલોડ, અથવા વેબહૂક ડિસ્પેચને પણ લાગુ પડે છે.

સાચું ક્રમ આ મુજબ છે:

  1. ટ્રાન્ઝેક્શન પૂર્ણ કરો અને તમને જરૂરી કોઈપણ ID અથવા પરિણામો મેળવો.
  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) લગભગ હંમેશા સફળ થાય છે. નેટવર્ક સ્થિર હોય છે, ડિસ્ક ક્યારેય ભરાતી નથી, અને કોઈ સ્પર્ધાત્મક લોડ હોતો નથી. કોડ સાચો લાગે છે કારણ કે તે સામાન્ય રીતે કામ કરે છે.

પ્રોડક્શન (Production) અલગ છે. બે ગ્રાહકો બરાબર એક જ મિલિસેકન્ડમાં ઓર્ડર સબમિટ કરે છે. ડિપ્લોયમેન્ટ દરમિયાન ક્યુ વર્કર (queue worker) કામની વચ્ચે જ રિસ્ટાર્ટ થઈ જાય છે. પેમેન્ટ પ્રોવાઈડર ત્રીસ સેકન્ડ માટે ટાઈમ આઉટ થઈ જાય છે. ટ્રાન્ઝેક્શન વગર, આ ઘટનાઓ ઓર્ફન રેકોર્ડ્સ (orphan records) અને ખોટા ટોટલ બનાવે છે જે શોધવા ખૂબ મુશ્કેલ હોય છે. સૌથી ખરાબ બાબત એ છે કે સ્ટાન્ડર્ડ ફીચર ટેસ્ટ્સ તેને ભાગ્યે જ પકડી શકે છે, કારણ કે નિષ્ફળતાનો પ્રકાર સમય (timing) પર આધારિત છે અને ઇન્ફ્રાસ્ટ્રક્ચર સાથે જોડાયેલ છે, સાદા લોજિક એરર્સ સાથે નહીં.

સુધારો કોઈ અઘરો નથી. તે મિકેનિકલ છે. જ્યારે તમે બે અથવા વધુ સંબંધિત રાઈટ્સ (writes) જુઓ, ત્યારે તેને ટ્રાન્ઝેક્શનમાં લપેટો (wrap). સમય જતાં, તે રિક્વેસ્ટ વેલિડેટ કરવા જેટલું જ ઓટોમેટિક બની જવું જોઈએ.

તેને એક રિફ્લેક્સ (reflex) બનાવો

DB::transaction() જટિલતા વધારતું નથી. તે ઘટના બન્યા પછી અધૂરા ડેટાને સાફ કરવાનો પ્રયાસ કરવાની છુપી જટિલતાને દૂર કરે છે. જો ડેટાબેઝ ઓપરેશન્સનો સમૂહ એકસાથે હોવો જોઈએ, તો શરૂઆતથી જ તેમની સાથે તે રીતે વર્તો. તમારું એપ્લિકેશન લોડ હેઠળ વિશ્વાસપાત્ર રહેશે, તમારા એરર લોગ્સ ક્લીન રહેશે, અને તમારો ડેટાબેઝ અધૂરા રેકોર્ડ્સનું સ્મશાન નહીં બને.