انتقال للمقال

مبدأ الـ Atomicity

السلام عليكم ورحمة الله وبركاته

وقت القراءة: ≈ 10 دقائق

المقدمة


هذا المبدأ يضمن لك أن الـ Transaction بأكملها يجب أن تنفذ كوحدة واحدة
بمعنى أنه إما تنفذها كلها أو أن لا تنفذ شيئا منها أي لو كانت الـ
Transaction تحتوي على 10 من الـ Query
فمبدأ الـ Atomicity يقول لك يا تنفذ كل الـ Query التي بداخل الـ
Transaction أي تنفذ الـ 10 كلها أو لا تنفذ أي واحدة منهم
يا أما الـ Transaction كلها تنجح أو يا أما كلها تفشل
بالتالي لو حصل أي مشكلة في الـ Transaction ولو خطأ بسيط في أي Query فإن الـ
Transaction كلها ستفشل وتتم إلغاؤها ويتم التراجع عن كل ما تم تنفيذه
وهو ما يسمى بـ ROLLBACK أي التراجع عن العمليات وكأن شيئًا لم يحدث

ولو كل الـ Query تم تنفيذها بنجاح ففي هذه الحالة فقط سيتم تطبيق الـ
Transaction بنجاح ويتم تطبيق كل الـ Query على قاعدة البيانات وهو ما يسمى بـ
COMMIT

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

مثال عملي على الـ Atomicity

لنأخذ مثال عملي حيث لدينا جدول يسمى Accounts وجدول آخر يسمى Histories
ونريد تنفيذ العمليات التالية داخل Transaction

BEGIN;

UPDATE Accounts SET balance = balance - 100 WHERE user_id = 1;
INSERT INTO Histories (user_id, amount) VALUES (1, -100);

UPDATE Accounts SET balance = balance + 100 WHERE user_id = 2;
INSERT INTO Histories (user_id, amount) VALUES (2, 100);

COMMIT;

في هذا المثال لو حدث أي خطأ في أي Query فإن الـ Transaction كلها
ستفشل ويتم التراجع عن كل ما تم تنفيذه
فمثلًا لو تم تنفيذ الـ Query الأولى بنجاح لكن حدث خطأ في الـ
Query الثانية

هنا وفقًا لمبدأ الـ Atomicity فإن الـ Transaction يجب أن يتم التراجع عن كل ما
تم تنفيذه فيها أي يتم تنفيذ ROLLBACK
بالتالي الـ Query الأولى ستتم التراجع عنها وكأنها لم تحدث ولن يتم تطبيق أي شيء
على قاعدة البيانات

وإذا تم تنفيذ كل الـ Query بنجاح فإن الـ Transaction ستنجح ويتم
تطبيق كل الـ Query على قاعدة البيانات وهذا ما يسمى بـ COMMIT

وهذا هو ببساطة مبدأ الـ Atomicity يضمن لك أن الـ Transaction ستنجح بشكل كامل
أو ستفشل بشكل كامل
وهو يعد أهم مبدأ في الـ ACID وهو الذي يعطي الهوية والفائدة الكبيرة للـ
Transactions

تطبيق بسيط لفائدة الـ Atomicity

سأقوم بشرح مثال بسيط لفائدة الـ Atomicity باستخدام الـ ORM الخاص بـ
Laravel لكي أقرب لك شيء من الواقع التي قد تواجهه بشكل يومي
لأنك غالبًا ما ستستخدم ORM في مشروعك وليس الـ SQL بشكل مباشر

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

public function signup(Request $request)
{
    // 1. Validate the input
    $request->validate([
        'name' => 'required|string',
        'email' => 'required|email|unique:users,email',
        'password' => 'required|string|min:8',
        'course_ids' => 'required|array|min:1',
        'course_ids.*' => 'required|integer|exists:courses,id',
    ]);

    // 2. Create the user into the database
    $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

    // 3. Assign student role to the user
    $user->assignRole(Role::STUDENT);

    // 4. Attach the courses to the user
    $user->attachCourses($request->course_ids);

    // 5. Generate a token for the user
    $token = $user->createAccessToken();

    // 6. Return a success response
    return response()->json([
        'message' => 'You have been registered successfully',
        'token' => $token,
        'user' => $user,
    ], 201);
}

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

وفي هذه الدالة نقوم بعمل الخطوات التالية:

  1. تقوم بالتحقق من البيانات المرسلة من الطالب
  2. تقوم بإنشاء الطالب في قاعدة البيانات
  3. تقوم بتعين رتبة المستخدم كـ STUDENT
  4. تقوم بربط الطالب بالدورات التي اختارها
  5. تقوم بإنشاء Token للطالب
  6. تقوم بإرجاع رسالة نجاح مع الـ Token وبيانات الطالب

على أي حال الدالة تقوم بعمل Signup كما ترى وكل شيء يبدو على ما يرام ... صحيح
؟
أظنك تعرف الإجابة ... ستقول أن هناك مشكلة قد تحدث .. أو بعض المشاكل التي قد تحدث

وهذه المشكلة هى أن دالة الـ Signup تحتوي على العديد من الخطوات والعمليات
وهناك احتمال وارد أن تفشل أحد العمليات أو تحدث خطأ في أحد الخطوات
فتخيل معين أنه تم تخزين بيانات الطالب بنجاح في قاعدة البيانات
وتم تعين رتبته كـ STUDENT بنجاح
ولكن حدث مشكلة ما في الخطوة الثالثة أثناء ربط الطالب بالدورات التي اختارها لأي
سبب كان مثلًا لديك Exception بسبب معين أيًا ما كان

هنا من وجه نظر الطالب سيجد أنه بعد ما حاول التسجيل جاءه Exception فتوهم أنه لم
يتم تسجيله بنجاح ولكن في الحقيقة لقد تم تسجيله بنجاح ولكن حدث خطأ في الخطوة
الثالثة فقط
بالتالي عندما يحاول التسجيل مرة أخرى سيجد أنه لا يستطيع لأن البريد الإلكتروني
موجود بالفعل في قاعدة البيانات
لذا سحاول عمل login لكن سيتفاجيء أن الدورات التي اختارها لم تتم إضافتها له،
لأنها لم يتم تسجيلها اثناء الـ signup بسبب الـ Exception التي حدثت

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

الحل باستخدام Transaction

هنا يأتي دور مبدأ الـ Atomicity
وهو أنك ستضع دالة الـ Signup كلها داخل Transaction
بحيث أنه عندما تنتهي الدالة من تنفيذ كل محتواها دون مشاكل فقط في هذه الحالة سيتم
تطبيق وتنفيذ كل شيء في الـ Database
وإذا حدث أي خطأ في أي جزء من الدالة فسيتم التراجع عن كل ما تم تنفيذه ولن يتم
تطبيق أي شيء على الـ Database

public function signup(Request $request)
{
    $request->validate([
        'name' => 'required|string',
        'email' => 'required|email|unique:users,email',
        'password' => 'required|string|min:6',
        'course_ids' => 'required|array|min:1',
        'course_ids.*' => 'required|integer|exists:courses,id',
    ]);

    // Wrap the main logic inside a transaction
    DB::transaction(function () use ($request) {
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        $user->assignRole(Role::STUDENT);
        $user->attachCourses($request->course_ids);
    });

    $token = $user->createAccessToken();
    return response()->json([
        'message' => 'You have been registered successfully',
        'token' => $token,
        'user' => $user,
    ], 201);
}

في Laravel يمكنك بسهولة وضع الدالة داخل Transaction بوضعها داخل
DB::transaction
الـ ORM التي تتبنى Laravel تقوم بتبسيط كل شيء لك وتقوم بتنفيذ الـ
Transaction بشكل تلقائي

لكن لاحظ شيء هنا، نحن لم نضع الدالة كلها داخل الـ Transaction بل قمنا بوضع
الجزء الرئيسي من الدالة داخل الـ Transaction
نحن نضع الأجزاء المتعلقة بالـ Database بمعنى أن عملية التحقق من بيانات
المستخدم لا تأثر على الـ Database
فسواء حصل Exception فلا يوجد شيء تم تخزينه أو تعديله أو حذفه في الـ Database
نحتاج للتراجع عنه

لذا ستلاحظ أننا وضعنا فقط الأجزاء الرئيسية التي نقوم فيها بإنشاء المستخدم
وتعيين له الرتبة وربطه بالدورات داخل الـ Transaction
هذه هى الأجزاء المركزية والمهمة والتي تؤثر على الـ Database

أما الأجزاء الأخرى مثل التحقق من البيانات وإنشاء الـ Token وإرجاع الرسالة
فهذه الأجزاء لا تؤثر على الـ Database ولذا لا داعي لوضعها داخل الـ
Transaction

طرق كتابة Transaction في Laravel

في Laravel يوجد طريقتين لوضع الـ Transaction

الطريقة الأولى: DB::transaction

استخدام DB::transaction مثل ما قمنا به في المثال السابق

DB::transaction(function () {
    // اكتب كودك الجميل هنا
});

الطريقة الثانية: beginTransaction و commit و rollback

استخدام DB::beginTransaction و DB::commit و
DB::rollback

DB::beginTransaction();

try {
    // اكتب كودك الجميل هنا

    DB::commit();
} catch (Exception $e) {
    DB::rollback();
}

مثال آخر لفائدة الـ Atomicity

لنأخذ مثال آخر لفائدة الـ Atomicity وهو مثال كبير قليلًا
لنفترض أن هناك شخص يقوم برفع منتج معين وعرضه للبيع على موقع معين
وهذا الموقع لديه دالة تسمى uploadProduct تقوم برفع المنتج وعرضه للبيع

public function uploadProduct(Request $request)
{
    // 1. Validate the input
    $request->validate([
        'name' => 'required|string',
        'price' => 'required|numeric',
        'quantity' => 'required|integer',
        'last_date_to_sell' => 'required|date',
        'hand_made' => 'required|boolean',
    ]);

    // 2. Create the product into the database
    $product = Product::create([
        'name' => $request->name,
        'price' => $request->price,
        'quantity' => $request->quantity,
        'last_date_to_sell' => $request->last_date_to_sell,
        'hand_made' => $request->hand_made,
    ]);

    // 3. Calculate the score of the product
    $statistic = StatisticService::getProductsStatistic();
    $score = ScoreService::calculate($product, $statistic);

    // 4. Update the product score board
    ProductsScoreBoard::create([
        'product_id' => $product->id,
        'statistic_id' => $statistic->id,
        'score' => $score,
    ]);

    // 5. Attach reviewers to the product
    ReviewerService::attachReviewers($product);

    // 6. Send notification to the admins
    NotificationService::sendProductUploadedNotification($product);

    // 7. Return a success response
    return response()->json([
        'message' => 'The product has been uploaded successfully',
    ], 201);
}

حسنًا أمعن النظر في الدالة السابقة وتأمل فيها جيدًا وقل لي ما هي المشكلة التي قد
تحدث هنا ؟

ماذا لو حدث خطأ في حساب الـ Score ؟
أي أن الدالة ScoreService::calculate($product); حدث بها خطأ ما وألقت
Exception
في هذه الحالة المنتج قد تم إنشاؤه بالفعل ولكن الـ Request لم يكتمل بشكل كامل
وبالتالي لم يتم حساب الـ Score وهكذا سيكون لديك منتج في قاعدة البيانات لكن ليس
لديه Score برغم من أنه قد حصل Exception
وبالتالي لن يتم ربط المراجعين للمنتج ولن يتم إرسال أي إشعارات أو تنبيهات
للمشرفين

أو ماذا لو الخطأ حدث في دالة attachReviewers ؟
أو في دالة sendProductUploadedNotification ؟
وهكذا من الأمور التي قد تحدث وتؤدي إلى عدم استكمال العملية بشكل كامل أو بشكل
صحيح ومتوقع

الحل باستخدام Atomicity

هنا يأتي دور مبدأ الـ Atomicity المستخدم في الـ Transactions
وهو أنك يجب أن تضع كل العمليات التي تريد تنفيذها داخل Transaction
بالتالي إذا حدث أي خطأ في أي جزء من الـ Transaction فسيتم التراجع عن كل ما تم
تنفيذه ولن يتم تطبيق أي شيء على الـ Database

public function uploadProduct(Request $request)
{
    $request->validate([
        'name' => 'required|string',
        'price' => 'required|numeric',
        'quantity' => 'required|integer',
        'last_date_to_sell' => 'required|date',
        'hand_made' => 'required|boolean',
    ]);

    // Wrap the main logic inside a transaction
    DB::transaction(function () use ($request) {
        $product = Product::create([
            'name' => $request->name,
            'price' => $request->price,
            'quantity' => $request->quantity,
            'last_date_to_sell' => $request->last_date_to_sell,
            'hand_made' => $request->hand_made,
        ]);

        $statistic = StatisticService::getProductsStatistic();
        $score = ScoreService::calculate($product, $statistic);

        ProductsScoreBoard::create([
            'product_id' => $product->id,
            'statistic_id' => $statistic->id,
            'score' => $score,
        ]);

        ReviewerService::attachReviewers($product);
        NotificationService::sendProductUploadedNotification($product);
    });

    return response()->json([
        'message' => 'The product has been uploaded successfully',
    ], 201);
}

هنا قمنا بوضع الأجزاء المهمة والتي تتعامل مع الـ Database داخل الـ
Transaction
وهي من لحظة إنشاء المنتج وحتى حساب الـ Score وتحديث المنتج بالـ Score
الجديد
لكي نضم أنه إذا حدث أي خطأ في أي جزء من الـ Transaction فسيتم التراجع عن كل ما
تم تنفيذه ولن يتم تطبيق أي شيء على الـ Database وهو ما نسميه بـ ROLLBACK

ملخص

  • مبدأ الـ Atomicity يضمن أن الـ Transaction تنفذ بالكامل أو لا تنفذ على الإطلاق
  • إذا حدث أي خطأ يتم عمل ROLLBACK والتراجع عن كل شيء
  • إذا نجحت كل العمليات يتم عمل COMMIT وتطبيق التغييرات
  • في Laravel استخدم DB::transaction لتطبيق هذا المبدأ بسهولة

وهذا ببساطة مبدأ الـ Atomicity أرجو أن تكون قد فهمته بشكل جيد