مبدأ الـ Atomicity
السلام عليكم ورحمة الله وبركاته
المقدمة
- مبادئ الـ ACID في إدارة الـ Transactions
- مبدأ الـ Atomicity (أنت هنا)
- مبدأ الـ Consistency
- مبدأ الـ Isolation - مستويات العزل
- مبدأ الـ Isolation - أنواع الـ Locks
- مبدأ الـ Durability
هذا المبدأ يضمن لك أن الـ 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 للطالب الجديد وتربطه بالدورات التي
اختارها أثناء التسجيل
وفي هذه الدالة نقوم بعمل الخطوات التالية:
- تقوم بالتحقق من البيانات المرسلة من الطالب
- تقوم بإنشاء الطالب في قاعدة البيانات
- تقوم بتعين رتبة المستخدم كـ
STUDENT - تقوم بربط الطالب بالدورات التي اختارها
- تقوم بإنشاء
Tokenللطالب - تقوم بإرجاع رسالة نجاح مع الـ
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 أرجو أن تكون قد فهمته بشكل جيد