تخيل بتسجل طلب شراء وبتقلل المخزون لو المخزون اتغير لكن تسجيل الطلب فشل هتلاقي البيانات بتحكي قصتين مختلفتين هنا بنحتاج Database Transaction علشان التغييرات المرتبطة تنجح مع بعض أو تتراجع مع بعض

معنى Begin و Commit و Rollback

بداية المعاملة بتحدد مجموعة التغييرات اللي هنتعامل معاها كوحدة واحدة ال Commit بتثبت التغييرات وال Rollback بتلغي التغييرات غير المثبتة داخل المعاملة حسب إمكانيات محرك قاعدة البيانات

المثال عن تغييرات قاعدة البيانات نفسها مش أي أثر خارجي البرنامج يعمله أثناء التنفيذ

مثال آمن للتجربة في الذاكرة

الكود التالي بيستخدم SQLite داخل الذاكرة فقط وبيجهز جدول تجريبي للمخزون هننقص الكمية ثم نتعمد حدوث خطأ ونشوف هل الكمية رجعت يستخدم المثال إضافة PDO SQLite في PHP

<?php
$db = new PDO('sqlite::memory:');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('CREATE TABLE stock (sku TEXT PRIMARY KEY, quantity INTEGER NOT NULL)');
$db->exec("INSERT INTO stock VALUES ('book', 2)");

try {
    $db->beginTransaction();
    $db->exec("UPDATE stock SET quantity = quantity - 1 WHERE sku = 'book'");
    throw new RuntimeException('Order could not be saved');
    $db->commit();
} catch (Throwable $error) {
    if ($db->inTransaction()) {
        $db->rollBack();
    }
    echo "Rolled back" . PHP_EOL;
}

echo $db->query("SELECT quantity FROM stock WHERE sku = 'book'")->fetchColumn() . PHP_EOL;
Rolled back
2

الكمية فضلت 2 لأن التغيير اتراجع السطر اللي بيرمي الخطأ موجود للتجربة فقط في تطبيق حقيقي الفشل ممكن يحصل أثناء تسجيل الطلب أو التحقق من قاعدة مهمة

ولما كل الخطوات تنجح

المثال المستقل التالي بيثبت التغيير بدل التراجع عنه

<?php
$db = new PDO('sqlite::memory:');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('CREATE TABLE stock (sku TEXT PRIMARY KEY, quantity INTEGER NOT NULL)');
$db->exec("INSERT INTO stock VALUES ('book', 2)");

try {
    $db->beginTransaction();
    $db->exec("UPDATE stock SET quantity = quantity - 1 WHERE sku = 'book'");
    $db->commit();
} catch (Throwable $error) {
    if ($db->inTransaction()) $db->rollBack();
    throw $error;
}

echo $db->query("SELECT quantity FROM stock WHERE sku = 'book'")->fetchColumn() . PHP_EOL;
1

المعاملة مش كفاية لكل مشاكل التزامن

لو شخصان بيشتروا آخر نسخة في نفس الوقت فقراءة الكمية ثم تقليلها من غير حماية مناسبة ممكن تؤدي لقرارين مبنيين على نفس القيمة القديمة

ممكن تحتاج قفل الصف أو تحديث مشروط يتحقق إن الكمية تكفي مع فحص عدد الصفوف المتأثرة داخل المعاملة الطريقة المناسبة بتعتمد على قاعدة البيانات ومستوى العزل وحالة الاستخدام

الإيميل والملف مش بيرجعوا مع قاعدة البيانات

لو بعت رسالة بريد ثم حصل Rollback الرسالة مش هتختفي من صندوق المستلم علشان كده افصل الإجراءات الخارجية وخطط لتنفيذها بعد نجاح الحفظ

لو تسليم الإجراء بعد الحفظ مهم جدا ففكر في سجل مهام محفوظ داخل نفس المعاملة زي Outbox ثم عامل ينفذه مع التعامل مع إعادة المحاولة بدل افتراض إن خطوة بعد Commit مستحيل تفشل

خلي المعاملة قصيرة

متفتحش معاملة وتقعد تستنى رد مستخدم أو طلب شبكة طويل لأنها ممكن تحتفظ بأقفال وتأخر شغل تاني اجمع المعلومات غير الحساسة للتزامن قبلها ثم نفذ الجزء المرتبط بالبيانات في وقت قصير

إزاي تختبرها

جرب النجاح وفشل خطوة في المنتصف وتأكد إن كل الجداول المرتبطة رجعت لحالتها السابقة اختبر ده على قاعدة معزولة وراجع التزامن بمحرك مناسب قبل الاعتماد على الحل في مشروع حقيقي

متستخدمش مسح قاعدة المشروع كطريقة لتجربة المعاملات المثال هنا مقصود يكون في الذاكرة علشان التجربة مستقلة عن أي بيانات فعلية

مصادر للتوسع