= Трансакции == Употреба на 'Трансакции' соодветно проектот Трансакциите може да се состојат од една операција за читање, запишување, бришење или ажурирање \\ или комбинација од нив. Трансакцијата во SQL се низа од една или повеќе SQL активности извршени како една функција.Главната идеја зад трансакцијата е дека сите промени направени во неа треба или да бидат целосно успешни или целосно да се поништат во случај на неуспех.\\ Во случај на грешка, враќањето осигурува да не се зачуваат делумни промени.\\ \\ Како што знаеме, трансакциите треба да ги содржат следниве 4 аспекти познати како ''' ACID''' : \\ '''-> Atomicity '''\\ Тоа би значело резултатот од трансакцијата може да биде или целосно успешен или целосно неуспешен.\\ Целата трансакција мора да се врати назад ако еден дел од неа не успее.\\ '''-> Consistency ''' \\ Трансакцијата ја претвора базата од една валидна состојба во друга валидна состојба. \\ Правилата и ограничувањата на базата секогаш остануваат запазени. \\ '''-> Isolation '''\\ Секоја трансакција се извршува како да е единствената во системот. \\ Паралелните трансакции не си влијаат меѓусебно додека се извршуваат. \\ На тој начин се гарантира точноста на податоците. \\ '''-> Durability ''' \\ Откако трансакцијата ќе биде потврдена, промените се трајно зачувани. \\ Дури и при пад на системот, податоците остануваат зачувани. \\ Исто така, да не заборавиме дека трансакциите се карактеристички по опфаќање на следниве 4 команди : \\ ''' BEGIN TRANSACTION -> COMMIT -> ROLLBACK -> SAVEPOINT ''' \\ \\ Поради тоа, на страната '''{{{app.js}}}''', го додаваме следново: \\ -> трансакцијата ја започнуваме со '''{{{await client.query('BEGIN');}}}''' \\ на овој начин сите операции кои следат ќе бидат извршени во контекст на една трансакција \\ -> ако сите операции во тек на трансакција бидат успешни, \\ правиме '''{{{COMMIT}}}''' со '''{{{await client.query('COMMIT');}}}''' \\ -> доколку се случи грешка во било кој дел од процесот, \\ сите промени ќе се поништат со '''{{{await client.query('ROLLBACK');}}}''' \\ -> ке имаме функционален '''{{{SAVEPOINT}}}''' и подобрена заштита од грешки во трансакциите, доколку по внесување нов '''{{{customers}}}''', ако има грешки во '''{{{transactions}}}''' \\ враќаме '''{{{ROLLBACK TO SAVEPOINT customer_added}}}''' наместо '''{{{целосен ROLLBACK}}}'''. \\ Ако се е во ред, правиме '''{{{COMMIT}}}''' и зачувуваме во база, \\ а ако има некоја друга грешка, правиме целосен '''{{{ROLLBACK}}}''' \\ \\ Соодветно темата и целта на мојот проект,трансакциите би требало да бидат имплементирани на овој начин: \\ -> Да можам да направам нарачка и намалам залиха во ист процес \\ -> Гаранција дека сите чекори во процесот ќе се извршат успешно или воопшто нема да се извршат \\ -> Можност за враќање на промените ако настане грешка \\ == Update = Atomicity Како што веќе кажавме, за еден успешен Atomicity потребно е имплементација на неколку клучни точки. \\ Користам '''{{{BEGIN }}}'''на почеток, потоа '''{{{COMMIT}}}'''ако помине успешно, и '''{{{ROLLBACK}}}'''ако има грешка.\\ Ако некој чекор не успее, во '''{{{catch}}}'''фаќаме грешки, повикуваме '''{{{ROLLBACK}}}''', \\ и трансакцијата се поништува а притоа ниту еднапромена не се зачувува.\\ Сега по поставам реални примери од фајлот '''app.js''' каде ги имам употребено истите за да докажеме Atomicity :\\ \\ '''Пример 1 : /order API'''\\ Првин трансакцијата почнува со {{{#!sql await client.query("BEGIN"); }}} \\ Потоа ги правам сите операции (провери, внеси, ажурирај залиха ...)\\ Ако некаде има грешка, се отфрла целата операција, и за крај :\\ {{{#!sql await client.query("ROLLBACK"); }}} \\ Доколку сите операции и обиди поминат успешно, се извршува :\\ {{{#!sql await client.query("COMMIT"); }}} \\ '''Пример 2 : Залиха'''\\ Сега ке видиме еден пример како што гласи правилото, трансакција мора да се врати назад ако еден дел од неа не успее.\\ Ако залихата за некој производ и големина фали,односно нема залиха, фрли грешка: {{{#!sql if (currentStock < item.quantity) { throw new Error(`Недоволна залиха за продукт ${item.product_name} (${item.size})`); } }}} \\ Ова е пример каде во '''catch ''' ке дојде некоја грешка и ке направи '''ROLLBACK'''\\ {{{#!sql await client.query("ROLLBACK"); }}} \\ '''Пример 3 : /order endpoint'''\\ Овде имам употреба на '''{{{BEGIN }}}''','''{{{COMMIT}}}''' и '''{{{ROLLBACK}}}'''.\\ {{{#!sql const client = await db.pool.connect(); try { await client.query("BEGIN"); // ... сите останати потребни чекори ... await client.query("COMMIT"); } catch (err) { await client.query("ROLLBACK"); } finally { client.release(); } }}} \\ '''Пример 4 : Try/catch'''\\ Секој чекор во процесот се наоѓа во блок '''try''', па да видиме пример за ажурирање на залиха :\\ {{{#!sql await client.query( "UPDATE stock SET quantity = quantity - $1, updated_at = NOW() WHERE id_stock = $2", [deductQty, stockRow.id_stock] ); }}} Кога овој момент нема да успее, ке премине во блок '''catch'''.\\ = Consistency Сега ке видиме пример каде ја штитиме базата од внесување невалидни или несоодветни податоци \\ поради поставените '''constraints''' односно ограничувања.\\ Воедно овие примери помагаат при оправдување на валидност на нашето сценарио каде при нарачување на продукт \\ кошничката нема да се дозволи додавање на продукт чија количина < 1.\\ \\ '''Пример 1 : Error'''\\ '''FOREIGN KEY врски''' (на пример, '''{{{id_customer}}}''' во '''{{{orders}}}''' мора да постои во '''{{{customers}}}'''),\\ а од друга страна имаме поставено и неколку '''NOT NULL''' полиња. \\ Во '''/order''' API, пред да се внесе нарачка, се проверува дали има доволно залиха : {{{#!sql if (currentStock < item.quantity) { throw new Error(`Недоволна залиха за продукт ${item.product_name} (${item.size})`); } }}} На овој начин се спречува несоодветно внесување на залиха во база, што би ја нарушило логиката и правилата во базата.\\ Ако нешто не е во ред, трансакцијата не се потврдува и базата останува во валидна состојба.\\ \\ '''Пример 2 : status(400) '''\\ Во API за промена на улога '''PUT /admin/users/:id/role''' проверуваm дали улогата постои во '''roles''' пред да се измени :\\ {{{#!sql const roleRes = await db.pool.query("SELECT id_role FROM roles WHERE role_name = $1", [role_name]); if (roleRes.rows.length === 0) { return res.status(400).json({ error: "Улогата не постои" }); } }}} ова е уште еден пример каде се штити базата од внесување невалидни или несоодветни податоци. \\ = Isolation За да оправдаме дека некоја трансакција се извршува како да е единствената во системот, притоа \\ паралелните трансакции да не си влијаат меѓусебно додека се извршуваат, е единствен начин се гарантира точност на податоците.\\ \\ '''Пример 1 : FOR UPDATE '''\\ Во '''/order''' функција, кога правам ажурирање на залиха '''stock''', користам '''FOR UPDATE''':\\ {{{#!sql const stocks = await client.query( `SELECT s.id_stock, s.quantity FROM stock s JOIN sizes sz ON s.id_size = sz.id WHERE s.id_product = $1 AND sz.size_label = $2 ORDER BY s.created_at ASC FOR UPDATE`, [item.productId, item.size] ); }}} '''FOR UPDATE''' го заклучува тој ред во базата за тековна трансакција,\\ и тогаш други паралелни трансакции не можат да го менуваат редот додека оваа трансакција не заврши.\\ На овој начин се спречува да има две нарачки кои истовремено ќе ги земат истите залихи,\\ што би довело до неконсистентна состојба.\\ = Durability Една од целите на трансакции е трајност,што при нарачување би значело дека откако трансакцијата ќе биде потврдена,\\ промените се трајно зачувани. Дури и при пад на системот, податоците остануваат зачувани.\\ {{{#!sql await client.query("COMMIT"); }}} Конкретно кај нас во база, овој ред гарантира дека сите промени во трансакцијата се трајно запишани во база.\\ Дури и серверот ако падне веднаш по '''COMMIT''', базата гарантира дека тие податоци ќе бидат зачувани.\\ = Сите ACID примери употребени на едно место Како што знаеме, и понатаму ке го тестираме сценариото од избор на производ до нарачување, \\ но ова е пример конкретно каде сите '''4 ACID''' примери се употребени на едно место : {{{#!sql const client = await db.pool.connect(); try { await client.query("BEGIN"); // Почеток на транзакција // Проверка и земање id на начин на плаќање const paymentMethodResult = await client.query("SELECT id_payment_method FROM payment_methods WHERE method_name = $1", [paymentMethod]); // Проверка и создавање/наоѓање на customer if (userId) { // Проверка дали постои customer со email // Ако не, вметни нов customer + адреса } else { // Вметни нов customer + адреса без email } // Вметни нова нарачка во orders табелата // За секој продукт во кошничката: // 1) SELECT SUM(quantity) од stock каде size и product одговараат // 2) Проверка дали има доволна залиха // 3) SELECT stock rows FOR UPDATE за заклучување // 4) UPDATE stock quantity (одземање залиха) // 5) INSERT во contains табелата (поврзување нарачка-продукти) // Вметни запис во payments табелата await client.query("COMMIT"); // Потврда на транзакцијата res.status(200).json({ message: "Нарачката е успешно завршена.", orderId }); } catch (err) { await client.query("ROLLBACK"); // Враќање назад ако има грешка res.status(500).json({ message: "Грешка при обработка на нарачката: " + err.message }); } finally { client.release(); } }}} Како што може да се види, имаме : \\ -> Почеток на трансакција со '''BEGIN''' \\ -> Потврда на трансакцијата со '''COMMIT''' \\ -> Доколку дојде до грешка, го скокнува '''COMMIT''' и доаѓа до '''CATCH''' \\ -> После '''CATCH''' доаѓа '''ROLLBACK''' односно враќање назад \\