- بواسطة x32x01 ||
مشكلة حجز الفنادق ليست مجرد تنفيذ
المشكلة الحقيقية تظهر عندما يحاول شخصان حجز نفس الغرفة في نفس الوقت، مع وجود فترة زمنية متداخلة.
إذا اعتمدت على كود التطبيق فقط للتحقق من توفر الغرفة، فقد يمر الطلبان معًا قبل تسجيل أي منهما، وينتهي بك الأمر بحجزين لنفس الغرفة.
في PostgreSQL يمكنك منع هذا السيناريو من مستوى قاعدة البيانات نفسها باستخدام
بعد ذلك أضف قيدًا يمنع وجود حجوزات متداخلة لنفس الغرفة:
المعنى ببساطة: نفس الغرفة + وقت متداخل = الحجز مرفوض.
وبذلك تصبح قاعدة البيانات نفسها مسؤولة عن حماية هذه القاعدة، بدل الاعتماد فقط على منطق الـ Application.
يعني أن القاعدة تنطبق عندما يكون الحجزان لنفس الغرفة.
أما:
فيحوّل وقت بداية ونهاية الحجز إلى Range زمني، ثم يستخدم العامل
وبالتالي لن يسمح PostgreSQL بوجود حجزين عندما يتحقق الشرطان:
مثلًا، إذا كان لدينا حجز من: 10:00 → 12:00
وحجز آخر من: 12:00 → 14:00
فلن يعتبر PostgreSQL أن هناك تداخلًا، لأن الحجز الثاني يبدأ بالضبط عند انتهاء الحجز الأول.
لكن إذا حاول مستخدم حجز الغرفة من: 11:00 → 13:00
فسيحدث تداخل مع الحجز الأول، وبالتالي سيرفض PostgreSQL الحجز.
إذا لم تُرجع العملية أي نتائج، يقوم التطبيق بإضافة الحجز.
لكن هنا توجد مشكلة مهمة وهي Race Condition.
تخيل أن مستخدمين يحاولان حجز الغرفة نفسها في نفس اللحظة.
المستخدم الأول ينفذ
وفي نفس الوقت، المستخدم الثاني ينفذ
بعدها يمكن للطلبين تنفيذ
إذن فحص التوفر داخل التطبيق وحده لا يكفي لضمان سلامة البيانات.
حتى لو وصلت عدة طلبات في نفس الوقت، PostgreSQL ستطبق الـ Constraint وتمنع الحجز الذي يتعارض مع حجز موجود.
مثلًا:
إذا كانت الغرفة 101 تحتوي بالفعل على حجز يتداخل مع هذه الفترة، فسيفشل
يمكن للتطبيق التعامل مع خطأ قاعدة البيانات وإظهار رسالة مناسبة للمستخدم مثل:
الغرفة غير متاحة في الفترة التي اخترتها.
داخل
يمكن تفعيله باستخدام:
أما
إذا كنت تستخدم
يمكن استخدام نفس الأسلوب في:
في PostgreSQL يمكنك جعل قاعدة البيانات نفسها تفرض قاعدة العمل باستخدام
نفس الغرفة + وقت متداخل = ممنوع.
وهذا يجعل حماية البيانات أقوى، خصوصًا عندما تصل عدة طلبات حجز في نفس الوقت.
الفكرة المهمة هنا أن قواعد العمل التي تؤثر مباشرة على سلامة البيانات يجب، عندما يكون ذلك مناسبًا، أن تكون محمية على مستوى قاعدة البيانات وليس فقط داخل كود التطبيق.
SELECT للتأكد من توفر الغرفة ثم INSERT لإضافة الحجز.المشكلة الحقيقية تظهر عندما يحاول شخصان حجز نفس الغرفة في نفس الوقت، مع وجود فترة زمنية متداخلة.
إذا اعتمدت على كود التطبيق فقط للتحقق من توفر الغرفة، فقد يمر الطلبان معًا قبل تسجيل أي منهما، وينتهي بك الأمر بحجزين لنفس الغرفة.
في PostgreSQL يمكنك منع هذا السيناريو من مستوى قاعدة البيانات نفسها باستخدام
EXCLUDE Constraint مع GiST.كيف تمنع PostgreSQL الحجوزات المتداخلة؟
أولًا، فعّل امتدادbtree_gist: SQL:
CREATE EXTENSION IF NOT EXISTS btree_gist; بعد ذلك أضف قيدًا يمنع وجود حجوزات متداخلة لنفس الغرفة:
SQL:
ALTER TABLE bookings
ADD CONSTRAINT no_overlapping_bookings
EXCLUDE USING gist (
room_id WITH =,
tstzrange(starts_at, ends_at, '[)') WITH &&
); المعنى ببساطة: نفس الغرفة + وقت متداخل = الحجز مرفوض.
وبذلك تصبح قاعدة البيانات نفسها مسؤولة عن حماية هذه القاعدة، بدل الاعتماد فقط على منطق الـ Application.
كيف يعمل EXCLUDE Constraint؟
هذا الجزء: SQL:
room_id WITH = أما:
SQL:
tstzrange(starts_at, ends_at, '[)') WITH && && لاكتشاف وجود تداخل بين الفترات.وبالتالي لن يسمح PostgreSQL بوجود حجزين عندما يتحقق الشرطان:
- نفس
room_id. - وجود تداخل بين الفترتين الزمنيتين.
لماذا نستخدم [) في الحجوزات؟
النطاق[) يعني أن وقت البداية مشمول في الفترة، بينما وقت النهاية غير مشمول.مثلًا، إذا كان لدينا حجز من: 10:00 → 12:00
وحجز آخر من: 12:00 → 14:00
فلن يعتبر PostgreSQL أن هناك تداخلًا، لأن الحجز الثاني يبدأ بالضبط عند انتهاء الحجز الأول.
لكن إذا حاول مستخدم حجز الغرفة من: 11:00 → 13:00
فسيحدث تداخل مع الحجز الأول، وبالتالي سيرفض PostgreSQL الحجز.
لماذا لا يكفي SELECT قبل INSERT؟
قد تستخدم الطريقة التقليدية التالية للتحقق من وجود حجز متداخل: SQL:
SELECT *
FROM bookings
WHERE room_id = 101
AND starts_at < '2026-10-15 14:00:00+00'
AND ends_at > '2026-10-15 10:00:00+00'; لكن هنا توجد مشكلة مهمة وهي Race Condition.
تخيل أن مستخدمين يحاولان حجز الغرفة نفسها في نفس اللحظة.
المستخدم الأول ينفذ
SELECT ولا يجد حجزًا.وفي نفس الوقت، المستخدم الثاني ينفذ
SELECT ولا يجد حجزًا أيضًا.بعدها يمكن للطلبين تنفيذ
INSERT، وبذلك تحصل على حجزين متداخلين.إذن فحص التوفر داخل التطبيق وحده لا يكفي لضمان سلامة البيانات.
لماذا EXCLUDE Constraint أفضل؟
عندما تضع قاعدة منع التداخل داخل PostgreSQL، تصبح هذه القاعدة جزءًا من سلامة البيانات نفسها.حتى لو وصلت عدة طلبات في نفس الوقت، PostgreSQL ستطبق الـ Constraint وتمنع الحجز الذي يتعارض مع حجز موجود.
مثلًا:
SQL:
INSERT INTO bookings (
room_id,
starts_at,
ends_at
)
VALUES (
101,
'2026-10-15 11:00:00+00',
'2026-10-15 13:00:00+00'
); INSERT بسبب no_overlapping_bookings.يمكن للتطبيق التعامل مع خطأ قاعدة البيانات وإظهار رسالة مناسبة للمستخدم مثل:
الغرفة غير متاحة في الفترة التي اخترتها.
ما فائدة btree_gist؟
عند استخدام: SQL:
room_id WITH = GiST، قد تحتاج إلى امتداد btree_gist عندما يكون room_id من نوع مثل integer.يمكن تفعيله باستخدام:
SQL:
CREATE EXTENSION IF NOT EXISTS btree_gist; أما
tstzrange فهو مناسب عندما تكون أعمدة الوقت من نوع timestamp with time zone.إذا كنت تستخدم
timestamp without time zone، فيمكنك استخدام tsrange بدلًا من tstzrange.أين يمكن استخدام هذه الطريقة؟
فكرة منع تداخل الفترات الزمنية لا تقتصر على الفنادق.يمكن استخدام نفس الأسلوب في:
- حجز غرف الفنادق.
- حجز السيارات.
- حجز المواعيد الطبية.
- حجز قاعات الاجتماعات.
- تأجير العقارات.
- حجز الموارد داخل الشركات.
- أي نظام لا يسمح باستخدام نفس المورد في فترتين متداخلتين.
الخلاصة
إذا كان نظامك يعتمد على الحجوزات، فلا تعتمد فقط علىSELECT للتحقق من توفر المورد قبل تنفيذ INSERT.في PostgreSQL يمكنك جعل قاعدة البيانات نفسها تفرض قاعدة العمل باستخدام
EXCLUDE Constraint:نفس الغرفة + وقت متداخل = ممنوع.
وهذا يجعل حماية البيانات أقوى، خصوصًا عندما تصل عدة طلبات حجز في نفس الوقت.
الفكرة المهمة هنا أن قواعد العمل التي تؤثر مباشرة على سلامة البيانات يجب، عندما يكون ذلك مناسبًا، أن تكون محمية على مستوى قاعدة البيانات وليس فقط داخل كود التطبيق.
