PostgreSQL: منع تداخل حجوزات الفنادق

x32x01
  • بواسطة x32x01 ||
  • #1
مشكلة حجز الفنادق ليست مجرد تنفيذ 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 &&
فيحوّل وقت بداية ونهاية الحجز إلى Range زمني، ثم يستخدم العامل && لاكتشاف وجود تداخل بين الفترات.

وبالتالي لن يسمح 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'
);
إذا كانت الغرفة 101 تحتوي بالفعل على حجز يتداخل مع هذه الفترة، فسيفشل 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:
نفس الغرفة + وقت متداخل = ممنوع.
وهذا يجعل حماية البيانات أقوى، خصوصًا عندما تصل عدة طلبات حجز في نفس الوقت.

الفكرة المهمة هنا أن قواعد العمل التي تؤثر مباشرة على سلامة البيانات يجب، عندما يكون ذلك مناسبًا، أن تكون محمية على مستوى قاعدة البيانات وليس فقط داخل كود التطبيق.
 
مواضيع مشابهة
x32x01
الردود
0
المشاهدات
20
x32x01
x32x01
x32x01
الردود
0
المشاهدات
42
x32x01
x32x01
إحصائيات المنتدى
المواضيع
2,279
المشاركات
2,344
الأعضاء
16
آخر عضو مسجل
HcN57
عودة
أعلى