7. إدارة الوصول المتزامن إلى البيانات
لقد استخدمنا حتى الآن جداول كنا نحن المستخدمين الوحيدين لها. في الواقع العملي، على جهاز متعدد المستخدمين، غالبًا ما يتم مشاركة البيانات بين مستخدمين مختلفين. وهنا يطرح السؤال التالي: من يمكنه استخدام جدول معين وبأي شكل (الاستعلام، الإدراج، الحذف، الإضافة، ...)؟
7.1. إنشاء مستخدمي Firebird
عندما عملنا مع IB-Expert، قمنا بتسجيل الدخول كمستخدم SYSDBA. يمكن العثور على هذه المعلومات في خصائص الاتصال المفتوح لـ SGBD:
![]() | ![]() |
على اليمين، نرى أن المستخدم المتصل هو [SYSDBA]. ما لا نراه هو كلمة مروره [masterkey]. [SYSDBA] هو مستخدم خاص في Firebird: فهو يتمتع بجميع الصلاحيات على جميع الكائنات التي يديرها SGBD. يمكن إنشاء مستخدمين جدد باستخدام IBExpert مع الخيار [Tools / User Manager] أو الرمز التالي:

تظهر لنا نافذة إدارة المستخدمين:

يتيح الزر [Add] إنشاء مستخدمين جدد:

لنقم إذن بإنشاء المستخدمين التاليين:
الاسم | كلمة المرور |
ADMIN1 | admin1 |
ADMIN2 | admin2 |
SELECT1 | select1 |
SELECT2 | select2 |
UPDATE1 | تحديث1 |
UPDATE2 | update2 |
7.2. منح حقوق الوصول للمستخدمين
تنتمي قاعدة البيانات إلى من أنشأها. كانت قواعد البيانات التي أنشأناها حتى الآن مملوكة للمستخدم [SYSDBA]. لتوضيح مفهوم الحقوق، لنقم بإنشاء (Database / Create Database) قاعدة بيانات جديدة تحت الهوية [ADMIN1, admin1]:

ونقوم بتسجيلها بالاسم المستعار DBACCES (ADMIN1). يتيح استخدام الأسماء المستعارة فتح اتصالات على نفس قاعدة البيانات مع منحها معرّفات مختلفة، مما يسهّل تحديدها في مستكشف قواعد البيانات لـ IBExpert:
![]() | ![]() |
لنقم الآن بإنشاء الجدولين التاليين: TA و TB:
الجدول TA
![]() |
الجدول TB
![]() |
لا توجد صلة بين هذه الجداول.
باستخدام IB-Expert، لنقم بإنشاء اتصال ثانٍ بقاعدة البيانات [DBACCES]، هذه المرة تحت الاسم [ADMIN2 / admin2]. ونستخدم لهذا الغرض الخيار [Database / Register Database]:
![]() | ![]() |
لننتقل إلى DBACCES (ADMIN2) ونفتح محرر SQL (Shift + F12):
![]() |
ستتاح لنا الفرصة لاستخدام اتصالات متنوعة على نفس قاعدة البيانات [DBACCES]. لكل منها، سيكون لدينا محرر SQL. في [1]، يشير المحرر SQL إلى الاسم المستعار للقاعدة المتصلة. استخدم هذه الإشارة لمعرفة المحرر SQL الذي تتواجد فيه. سيكون لهذا الأمر أهمية لأننا سننشئ اتصالات لن تتمتع بنفس حقوق الوصول إلى كائنات قاعدة البيانات.
لنطلب محتوى الجدول TA:

نحصل على رسالة الخطأ التالية:

ما معنى ذلك؟ تم إنشاء قاعدة البيانات [DBACCESS] بواسطة المستخدم [ADMIN1]، وبالتالي فهي ملكه. وهو وحده من يمكنه الوصول إلى الكائنات المختلفة في هذه القاعدة. ويمكنه منح حقوق الوصول لمستخدمين آخرين باستخدام الأمر SQL GRANT. ولهذا الأمر صيغ مختلفة. وإحدى هذه الصيغ هي التالية:
GRANT الامتياز1، الامتياز2، ...| ALL PRIVILEGES ON table/vue TO المستخدم1، المستخدم2، ...| PUBLIC [ WITH GRANT OPTION ] | |
يمنح امتيازات الوصول privilègei أو جميع الامتيازات (ALL PRIVILEGES) على table أو vue للمستخدمين utilisateuri أو لجميع المستخدمين ( PUBLIC ). تسمح الفقرة WITH GRANT OPTION للمستخدمين الذين حصلوا على الامتيازات بنقلها بدورهم إلى مستخدمين آخرين. |
ومن بين الامتيازات privilègei التي يمكن منحها ما يلي:
حق استخدام الأمر DELETE على الجدول أو العرض. | |
حق استخدام الأمر INSERT على الجدول أو العرض | |
حق استخدام الأمر SELECT على الجدول أو العرض | |
حق استخدام الأمر UPDATE على الجدول أو العرض. يمكن تقييد هذا الحق ليشمل أعمدة معينة باستخدام الصيغة التالية: GRANT تحديث (col1، col2، ...) ON الجدول/العرض TO المستخدم1، المستخدم2، ...| PUBLIC [ WITH GRANT OPTION ] |
لنمنح المستخدم [ADMIN2] الحق SELECT على الجدول TA. لا يمكن لمنح هذا الحق سوى مالك الجدول، c.a.d. هنا [ADMIN1]. لننتقل إلى الاتصال DBACCES(ADMIN1) ونفتح محررًا جديدًا SQL (Shift+F12):

بعد ذلك، سننتقل من محرر SQL إلى المحرر الآخر. للتنقل بينهما، يمكننا استخدام الخيار [Windows] من القائمة:

في الأعلى، نرى المحررين SQL، كل منهما مرتبط بمستخدم معين. لنعد إلى المحرر SQL(ADMIN1) ونصدر الأمر التالي:

ثم نؤكدها باستخدام الأمر COMMIT:

بعد ذلك، ننتقل إلى محرر المستخدم ADMIN2 لإعادة إنشاء SELECT الذي فشل:

تظهر لنا رسالة الخطأ التالية:

لا يزال المستخدم [ADMIN2] غير مخول بالاطلاع على الجدول [TA]. في الواقع، يبدو أن صلاحيات المستخدم يتم تحميلها عند تسجيل دخوله. وبالتالي، سيظل [ADMIN2] يتمتع بنفس الصلاحيات التي كان يتمتع بها عند بدء تسجيل دخوله، أي لا صلاحيات على الإطلاق. دعونا نتحقق من ذلك. لنقوم بفصل المستخدم [ADMIN2]:
- انتقل إلى اتصاله
- اطلب إنهاء الاتصال بالنقر بزر الماوس الأيمن على الاتصال واختيار الخيار [Deconnect from database] أو (Shift + Ctrl + D)

إذا طلبت إحدى اللوحات إدخال [COMMIT]، فقم بإدخال [COMMIT]. ثم أعد تسجيل دخول المستخدم [ADMIN2] عن طريق اختيار الخيار [Reconnect] المذكور أعلاه. بعد الانتهاء من ذلك، لنعد إلى المحرر SQL (ADMIN2) ونعيد تشغيل الطلب SELECT الذي فشل:

ونحصل عندئذٍ على النتيجة التالية:

هذه المرة، يمكن لـ ADMIN2 الاطلاع على الجدول TA بفضل الصلاحية SELECT التي منحها له مالكه ADMIN1. عادةً ما يكون هذا هو الحق الوحيد الذي يمتلكه. دعونا نتحقق من ذلك. ما زلنا في محرر SQL (ADMIN2):
![]() | ![]() |
تُظهر الشاشة اليمنى أن ADMIN2 لا يمتلك الحق DELETE على الجدول TA.
لنعد إلى محرر SQL (ADMIN1) لمنح المزيد من الصلاحيات للمستخدم ADMIN2. نقوم بتنفيذ الأمرين التاليين بالتتابع:
![]() | ![]() |
- يمنح الأمر الأول المستخدم ADMIN2 جميع حقوق الوصول إلى الجدول [TA] بالإضافة إلى إمكانية منحه هو أيضًا حقوقًا (WITH GRANT OPTION)
- الأمر الثاني يؤكد صحة الأمر السابق
وبعد ذلك، وكما فعلنا سابقًا، نجدد اتصال المستخدم [ADMIN2] (قطع الاتصال / إعادة الاتصال)، ثم في المحرر SQL (ADMIN2) نكتب الأوامر التالية:
![]() | ![]() | ![]() |
تمكن ADMIN2 من حذف جميع الصفوف من الجدول TA. لنلغِ هذا الحذف باستخدام ROLLBACK:
![]() | ![]() | ![]() |
لنتحقق من أن ADMIN2 يمكنه بدوره منح حقوق على الجدول TA.
![]() | ![]() |
لنقم الآن بإنشاء اتصال بقاعدة البيانات [DBACCES] (قاعدة البيانات / تسجيل قاعدة البيانات) باسم [SELECT1 / select1]، وهو أحد المستخدمين الذين تم إنشاؤهم سابقًا، ثم نضغط مرتين على الرابط الذي تم إنشاؤه في [Database Explorer]:
![]() | ![]() |
لننتقل إلى هذا الاتصال الجديد ونفتح محررًا جديدًا SQL (Shift + F12) لكتابة الأوامر التالية فيه:
![]() | ![]() |
يمتلك المستخدم SELECT1 حق الوصول SELECT إلى الجدول TA. هل يمكنه نقل هذا الحق إلى المستخدم SELECT2؟
![]() |
فشلت العملية لأن المستخدم SELECT1 لم يحصل على حق نقل الحق SELECT الذي حصل عليه من المستخدم ADMIN2. ولذلك كان من الضروري أنالمستخدم ADMIN2 يستخدم الشرط WITH GRANT OPTION في أمره SQL GRANT. قواعد النقل بسيطة:
- لا يمكن للمستخدم أن ينقل سوى الحقوق التي تلقّاها، ولا أكثر
- ولا يمكنه نقلها إلا إذا كان قد تلقّاها مع الامتياز [WITH GRANT OPTION]
يمكن سحب حق مُمنوح باستخدام الأمر REVOKE:
REVOKE الامتياز1، الامتياز2، ...| ALL PRIVILEGES ON table/vue FROM المستخدم1، المستخدم2، ...| PUBLIC | |
يلغي امتيازات الوصول privilègei أو جميع الامتيازات (ALL PRIVILEGES) على table أو vue للمستخدمين utilisateuri أو لجميع المستخدمين ( PUBLIC ). |
لنجرب ذلك. لنعد إلى محرر SQL الخاص بـ ADMIN2 لإزالة الصلاحية SELECT التي منحناها للمستخدم SELECT1:
![]() | ![]() |
لنقطع الاتصال ثم نعيد توصيل اتصال المستخدم SELECT1. ثم في المحرر SQL (SELECT1) نطلب محتوى الجدول TA:
![]() | ![]() |
لقد فقد المستخدم SELECT1 بالفعل حق القراءة في الجدول TA. تجدر الإشارة إلى أن ADMIN2 هو الذي منحه هذا الحق، وأن ADMIN2 هو الذي سحبه منه. وإذا حاول المستخدم ADMIN1 سحب هذا الحق منه، فلن يتم الإبلاغ عن أي خطأ، ولكن يمكن ملاحظة لاحقًا أن المستخدم SELECT1 قد احتفظ بحقه في الجدول SELECT.
يمكن منح حق للجميع باستخدام الصيغة التالية: GRANT حق (حقوق) ON الجدول / العرض TO PUBLIC. لنمنح إذن SELECT على الجدول TA للجميع. يمكن استخدام ADMIN1 أو ADMIN2 للقيام بذلك. نستخدم ADMIN2:
![]() | ![]() |
لنقم بإنشاء اتصال بالقاعدة باستخدام المستخدم USER1 / user1:
![]() | ![]() |
باستخدام جلسة الاتصال DBACCES (USER1)، لنفتح محررًا جديدًا SQL (Shift + F12) ونكتب الأوامر التالية:
![]() | ![]() |
يتمتع المستخدم USER1 بالفعل بالحق SELECT على الجدول TA.
7.3. المعاملات
7.3.1. مستويات العزل
ننتقل الآن من مسألة حقوق الوصول إلى كائنات قاعدة البيانات إلى مسألة الوصول المتزامن إلى هذه الكائنات. يفترض أن مستخدمين اثنين يتمتعان بحقوق وصول كافية إلى كائن في قاعدة البيانات، مثل جدول على سبيل المثال، يرغبان في استخدامه في نفس الوقت. ماذا يحدث؟
يعمل كل مستخدم ضمن معاملة. والمعاملة هي سلسلة من الأوامر SQL التي يتم تنفيذها بشكل «أتمي»:
- إما أن تنجح جميع العمليات
- إما أن تفشل إحدى العمليات، وفي هذه الحالة يتم إلغاء جميع العمليات التي سبقتها
في النهاية، إما أن تكون جميع عمليات المعاملة قد نُفذت بنجاح، أو لم تُنفذ أي منها على الإطلاق. عندما يكون المستخدم هو نفسه المتحكم في المعاملة (وهذا هو الحال في هذا المستند بأكمله)، فإنه يثبت المعاملة بأمر COMMIT أو يلغيها بأمر ROLLBACK.
يعمل كل مستخدم ضمن معاملة خاصة به. وعادةً ما يتم التمييز بين أربعة مستويات من العزل بين المستخدمين المختلفين:
- قراءة غير ملتزم بها
- القراءة الملتزم بها
- قراءة قابلة للتكرار
- قراءة قابلة للتسلسل
قراءة غير ملتزم بها
يُعرف وضع العزل هذا أيضًا باسم "القراءة غير النظيفة". فيما يلي مثال على ما قد يحدث في هذا الوضع:
- يبدأ المستخدم U1 معاملة على الجدول T
- يبدأ المستخدم U2 معاملة على نفس الجدول T
- يقوم المستخدم U1 بتعديل صفوف في الجدول T ولكنه لم يقم بتثبيتها بعد
- المستخدم U2 «يرى» هذه التعديلات ويتخذ قرارات بناءً على ما يراه
- يقوم المستخدم بإلغاء معاملته بواسطة ROLLBACK
نلاحظ أنه في الخطوة 4، اتخذ المستخدم U2 قرارًا بناءً على بيانات ستتضح لاحقًا أنها خاطئة.
القراءة الملتزم بها
يُجنّب وضع العزل هذا المأزق السابق. في هذا الوضع، لن «يرى» المستخدم U2 في الخطوة 4 التعديلات التي أجراها المستخدم U1 على الجدول T. ولن يراها إلا بعد أن يقوم المستخدم U1 بإجراء عملية COMMIT لمعاملته.
في هذا الوضع، الذي يُعرف أيضًا باسم «القراءة غير القابلة للتكرار» (Unrepeatable Read)، قد تحدث الحالات التالية:
- يبدأ المستخدم U1 معاملة على الجدول T
- يبدأ المستخدم U2 معاملة على نفس الجدول T
- يقوم المستخدم U2 بإجراء عملية SELECT للحصول على متوسط العمود C للصفوف في T التي تستوفي شرطًا معينًا
- يقوم المستخدم U1 بتعديل (UPDATE) بعض القيم في العمود C من T ثم يثبتها (COMMIT)
- يقوم المستخدم U2 بإعادة تنفيذ نفس العملية SELECT المذكورة في النقطة 3. وسيكتشف أن متوسط العمود C قد تغير بسبب التعديلات التي أجراها U1.
الآن لا يرى المستخدم U2 سوى التعديلات التي «تمت الموافقة عليها» من قِبل U1. ولكن بينما لا يزال ضمن نفس المعاملة، تُسفر عمليتان متطابقتان (3 و5) عن نتائج مختلفة. يُطلق مصطلح «قراءة غير قابلة للتكرار» (Unrepeatable Read) على هذه الحالة. وهي حالة مزعجة لمن يرغب في الحصول على صورة ثابتة للجدول T.
القراءة القابلة للتكرار
في وضع العزل هذا، يُضمن للمستخدم الحصول على نفس النتائج عند قراءاته للقاعدة طالما بقي ضمن نفس المعاملة. فهو يعمل على نسخة لا تنعكس عليها أبدًا التعديلات التي أجرتها المعاملات الأخرى، حتى لو تم تأكيدها. ولن يرى هذه التعديلات إلا عندما ينهي هو نفسه معاملته باستخدام COMMIT أو ROLLBACK.
ومع ذلك، فإن وضع العزل هذا ليس مثاليًا بعد. بعد العملية 3 المذكورة أعلاه، يتم قفل السطور التي استعلام عنها المستخدم U2. أثناء العملية 4، لن يتمكن المستخدم U1 من تعديل (UPDATE) قيم العمود C في هذه الصفوف. ومع ذلك، يمكنه إضافة صفوف (INSERT). إذا كانت بعض الأسطر المضافة تستوفي الشرط الذي تم اختباره في الخطوة 3، فإن العملية 5 ستعطي متوسطًا مختلفًا عن المتوسط الذي تم الحصول عليه في الخطوة 3 بسبب الأسطر المضافة.
لحل هذه المشكلة الجديدة، يجب التبديل إلى وضع العزل «Serializable».
Serializable
في وضع العزل هذا، تكون المعاملات معزولة تمامًا عن بعضها البعض. ويضمن أن تؤدي المعاملتان اللتان تُجرىان في وقت واحد إلى نفس النتيجة التي كانت ستتحقق لو تم إجراؤهما واحدة تلو الأخرى. ولتحقيق هذه النتيجة، في العملية 4 حيث يرغب المستخدم U1 في إضافة أسطر من شأنها تغيير نتيجة المعاملة SELECT الخاصة بالمستخدم U1، سيتم منعه من ذلك. وستظهر له رسالة خطأ تفيد بأن الإدراج غير ممكن. وسيصبح الإدراج ممكنًا عندما يقوم المستخدم U2 بتأكيد معاملته.
مستويات عزل المعاملات الأربعة SQL ليست متاحة في جميع SGBD. يوفر Firebird مستويات العزل التالية:
- snapshot: وضع العزل الافتراضي. يتوافق مع وضع «Repeatable Read» في المعيار SQL.
- committed read: يتوافق مع وضع "committed read" في المعيار SQL
يتم تعيين مستوى العزل هذا بواسطة الأمر SET TRANSACTION:
SET TRANSACTION [READ WRITE | READ ONLY] [WAIT|NOWAIT] ISOLATION LEVEL [SNAPSHOT | READ COMMITTED] | |
الكلمات الرئيسية التي تحتها خط هي القيم الافتراضية READ WRITE: يمكن للمعاملة القراءة والكتابة READ ONLY: لا يمكن للمعاملة سوى القراءة WAIT: في حالة حدوث تعارض بين معاملتين، فإن المعاملة التي لم تتمكن من تنفيذ العملية تنتظر حتى يتم المصادقة على المعاملة الأخرى. ولا يمكنها بعد ذلك إصدار أوامر SQL. NOWAIT: المعاملة التي لم تتمكن من تنفيذ العملية لا يتم حظرها. تتلقى رسالة خطأ ويمكنها مواصلة العمل. ISOLATION LEVEL [SNAPSHOT | READ COMMITTED]: مستوى العزل |
لنجرب. في محرر SQL(ADMIN1)، نكتب الأمر SQL التالي:

نلاحظ أن الأمر لم يتم قبوله. ولا نعرف السبب...
يتيح برنامج IB-Expert تحديد وضع العزل بطريقة أخرى. لنضغط بزر الفأرة الأيمن على الاتصال DBACCES(ADMIN1) لاختيار الخيار [Database Registration Info]:
![]() | ![]() |
تُظهر الشاشة اليمنى وجود خيار [Transactions]. سيسمح لنا هذا الخيار بتحديد مستوى عزل المعاملات. سنحدده هنا على [snapshot]. ونفعل الشيء نفسه مع الاتصال DBACCES (ADMIN2).
7.3.2. وضع اللقطة (snapshot)
لنلقِ نظرة على مستوى العزل snapshot، وهو وضع العزل الافتراضي في Firebird. عندما يبدأ المستخدم معاملة، يتم التقاط لقطة من قاعدة البيانات. ثم يعمل المستخدم على هذه اللقطة. وبالتالي، يعمل كل مستخدم على لقطة خاصة به من قاعدة البيانات. وإذا أجرى تعديلات عليها، فلن يراها المستخدمون الآخرون. ولن يروها إلا بعد أن يقوم المستخدم الذي أجراها بتأكيدها باستخدام COMMIT.
يمكننا النظر في حالتين:
- يقوم أحد المستخدمين بقراءة الجدول (select) بينما يقوم مستخدم آخر بتعديله (insert، update، delete)
- يرغب كلا المستخدمين في تعديل الجدول في نفس الوقت
7.3.2.1. مبدأ القراءة المتسقة
لنفترض أن هناك مستخدمين هما U1 و U2 يعملان على نفس الجدول TAB:
تبدأ معاملة المستخدم U1 في الوقت T1a وتنتهي في الوقت T1b.
تبدأ معاملة المستخدم U2 في الوقت T2a وتنتهي في الوقت T2b.
يعمل U1 على صورة لـ TAB التقطت في الوقت T1a. بين T1a و T1b، يقوم بتعديل TAB. ولن يتمكن المستخدمون الآخرون من الوصول إلى هذه التعديلات إلا في الوقت T1b، عندما يقوم U1 بعمل COMMIT.
يعمل U2 على صورة لـ TAB التقطت في الوقت T2a، وبالتالي فهي نفس الصورة التي استخدمها U1 (ما لم يقم مستخدمون آخرون بتعديل النسخة الأصلية في غضون ذلك). وهو لا «يرى» التعديلات التي قد يكون أجراها المستخدم U1 على TAB. ولن يتمكن من رؤيتها إلا في الوقت T1b.
لنوضح هذه النقطة باستخدام قاعدتنا [DBACCES]. سنجعل المستخدمين [ADMIN1] و [ADMIN2] يعملان في وقت واحد. لننتقل إلى الاتصال DBACCES (ADMIN1) وفي محرر SQL الخاص بـ ADMIN1، ولنقم بالعمليات التالية:
![]() | ![]() | ![]() |
قام ADMIN1 بتعديل السطر رقم 2 في الجدول TA ولكنه لم يقم بعد بتأكيد (COMMIT) العملية التي أجراها. ثم يقوم المستخدم ADMIN2 بإجراء عملية SELECT على الجدول TA (ننتقل في المحرر من SQL إلى ADMIN2). نحن الآن قبل الوقت T2a المذكور في المثال.
![]() | ![]() |
العودة إلى محرر SQL من ADMIN1 الذي يثبت إضافة العنصر:
![]() |
العودة إلى محرر SQL الخاص بـ ADMIN2 لإعادة إنشاء SELECT:
![]() | ![]() |
يرى ADMIN2 التعديلات التي أجراها ADMIN1. في وضع اللقطة (snapshot)، لا ترى المعاملة التعديلات التي أجرتها المعاملات الأخرى ما لم تكن هذه الأخيرة قد اكتملت.
7.3.2.2. التعديل المتزامن من قبل معاملتين لنفس الكائن في قاعدة البيانات
لنأخذ مثالاً في مجال المحاسبة: تعمل المعاملتان U1 و U2 على الحسابات. تقوم المعاملة U1 بخصم مبلغ S من رصيد المعاملة comptex وإيداع المبلغ نفسه في رصيد المعاملة comptey. وستقوم بذلك على عدة خطوات:
يبدأ U1 معاملة في الوقت T1a، ويخصم من حساب comptex في الوقت T1b، ويضيف المبلغ نفسه إلى حساب comptey في الوقت T1c، ويصادق على العمليتين في الوقت T1d. لنفترض أيضًا أن U2 يرغب في القيام بنفس الشيء، فيبدأ معاملته في الوقت T2a وينهيها في الوقت T2d وفقًا للمخطط التالي:
--------+----------+----+----+-------+------+-----+-------+---------
T1a T1b T2a T1c T2b T1d T2c T2d
في الوقت T2، يتم التقاط لقطة من جدول الحسابات لـ U2. وهي متسقة وفقًا لمبدأ snapshot. يرى U2 الحالة الأولية للحسابات comptex وcomptey لأن U1 لم يقم بعد بالتصديق على معاملاته.
لنفترض أن comptex لديه رصيد أولي قدره 1000 يورو، وأن كل من المستخدمين U1 وU2 يرغبان في خصم 100 يورو من رصيده.
- في الوقت T1b، يقوم U1 بخصم 100 يورو من رصيد comptex، وبذلك يصبح رصيده 90 يورو. ولن يتم إقرار هذه العملية إلا في الوقت T1d.
- في الوقت T2b، يرى U2 أن رصيد comptex يبلغ 1000 يورو (مبدأ القراءة المتسقة)، فيخصم منه 100 يورو، وبذلك يصبح الرصيد 90 يورو.
- في النهاية، في الوقت T2d عندما يتم التحقق من صحة كل شيء، سيكون رصيد comptex 90 يورو بدلاً من 80 يورو المتوقعة.
الحل لهذه المشكلة هو منع U2 من تعديل comptex طالما أن U1 لم تنتهِ من معاملتها. وبالتالي، سيتم حظر U2 حتى الوقت T1d. ويوفر الوضع snapshot هذه الآلية.
لنوضح ذلك باستخدام قاعدة البيانات DBACCES. يبدأ ADMIN1 معاملة في محرره SQL (ADMIN1):
![]() | ![]() | ![]() | ![]() |
بدأنا بإجراء معاملة COMMIT للتأكد من بدء معاملة جديدة. ثم قمنا بحذف السطر رقم 4. لم يتم المصادقة على المعاملة بعد.
يبدأ ADMIN2 بدوره معاملة في محرره SQL (ADMIN2):
![]() | ![]() |
تُظهر الشاشة اليمنى أن ADMIN2 أراد تعديل السطر رقم 4. وتم الرد عليه بأن ذلك غير ممكن لأن شخصًا آخر قد عدّله بالفعل ولكنه لم يقم بعد بالموافقة على هذا التعديل.
لنعد إلى محرر SQL (ADMIN1) لإجراء التعديل على COMMIT:

لنعد إلى محرر SQL(ADMIN2) لإعادة تنفيذ الأمر UPDATE:
![]() | ![]() |
![]() | ![]() |
تتم العملية UPDATE بنجاح على الرغم من أن السطر رقم 4 لم يعد موجودًا، كما يوضح الأمر SELECT التالي. وفي هذه اللحظة يكتشف الأمر ADMIN2 أن السطر لم يعد موجودًا.
7.3.2.3. وضع القراءة القابلة للتكرار (Repeatable Read)
لنوضح الآن وضع «القراءة القابلة للتكرار». يتم توفير هذا المستوى من العزل من خلال وضع «اللقطة». وهو يضمن حصول المعاملة دائمًا على نفس النتيجة عند قراءة قاعدة البيانات.
لنبدأ بالعمل مع محرر SQL الخاص بـ ADMIN2:
![]() | ![]() | ![]() |
![]() | ![]() |
لننتقل الآن إلى محرر SQL الخاص بـ ADMIN1:
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() | ![]() |
أضاف المستخدم ADMIN1 سطرين وقام بتأكيد معاملته. لنعد الآن إلى المحرر SQL (ADMIN2) لإعادة تشغيل SELECT SUM:
![]() | ![]() |
نلاحظ أن ADMIN2 لا يرى الأسطر المضافة في ADMIN1 على الرغم من أنها تمت المصادقة عليها بواسطة COMMIT. يُعطِي SELECT SUM نفس النتيجة التي كانت قبل الإضافات. هذا هو مبدأ «القراءة القابلة للتكرار» (Repeatable Read).
الآن، وما زلنا في المحرر SQL (ADMIN2)، دعونا نثبت المعاملة باستخدام COMMIT ثم نعيد تشغيل SELECT و SUM:
![]() | ![]() | ![]() |
يتم الآن أخذ الأسطر التي أضافها ADMIN1 في الاعتبار.
7.3.3. وضع «Committed Read»
لنوضح الآن وضع "Committed Read". هذا المستوى من العزل مشابه لمستوى snapshot باستثناء ما يتعلق بـ "Repeatable Read".
نبدأ بتغيير مستوى عزل المعاملات لكلتا الاتصالين.
- نقوم بقطع اتصال المستخدمين ADMIN1 و ADMIN2
- نقوم بتغيير مستوى عزل معاملاتهما

- نقوم بإعادة توصيل المستخدمين ADMIN1 و ADMIN2
نعود الآن إلى المثال السابق الذي يوضح "القراءة القابلة للتكرار" (Repeatable Read) لإظهار أن السلوك لم يعد كما كان. لنبدأ بالعمل مع المحرر SQL التابع لـ ADMIN2:
![]() | ![]() | ![]() |
![]() | ![]() |
لننتقل الآن إلى محرر SQL الخاص بـ ADMIN1:
![]() | ![]() | ![]() |
![]() | ![]() | ![]() |
![]() | ![]() |
أضاف المستخدم ADMIN1 سطرين وقام بتأكيد معاملته. لنعد الآن إلى المحرر SQL (ADMIN2) لإعادة تشغيل SELECT SUM:
![]() | ![]() |
لا يعطي SELECT SUM نفس النتيجة التي كانت قبل الإضافات التي أجراها ADMIN1. وهذا هو الفرق بين وضعي «snapshot» و«read committed».








































































