Menu
flag Ar iconالعربيةdown icon

المعاملات في SQLite: BEGIN وCOMMIT وROLLBACK

كيف تعمل المعاملات (Transactions) في SQLite: شرح BEGIN وCOMMIT وROLLBACK ووضع autocommit، والفرق بين DEFERRED وIMMEDIATE وEXCLUSIVE وتوقيت أخذ الأقفال.

تحتوي هذه الصفحة على محررات قابلة للتشغيل — حرّر، شغّل، وشاهد النتيجة فوراً.

المعاملات في sqlite: إما الكل أو لا شيء

المعاملة (transaction) في sqlite هي طريقة لتجميع عدة جمل SQL بحيث تُنفَّذ كلها معًا، أو لا يُنفَّذ أي منها. لو حصل أي خطأ في منتصف الطريق، تقدر ترجع (rollback) وترجع قاعدة البيانات لحالتها الأصلية تمامًا قبل ما تبدأ.

أشهر مثال على ذلك هو تحويل الأموال بين حسابين:

العمليتان UPDATE مرتبطتان ببعضهما. لو انهارت قاعدة البيانات بينهما، لخسرت Ada مبلغ 2000 سنت دون أن يحصل Boris على أي شيء بالمقابل. تغليفهما داخل BEGIN ... COMMIT يجعل الزوج ذرّيًا (atomic) — إما أن تتم العمليتان معًا، أو لا تتم أيٌّ منهما.

وضع autocommit في sqlite: السلوك الافتراضي الذي تستخدمه أصلًا

كل جملة SQL نفّذتها حتى الآن كانت في الحقيقة معاملة (transaction) قائمة بذاتها. فـ SQLite يعمل افتراضيًا في وضع autocommit: كل جملة تُلَف تلقائيًا بـ BEGIN وCOMMIT ضمنيين خاصين بها.

ثلاث عمليات إدخال، وثلاث معاملات منفصلة، وثلاث رحلات إلى القرص لتنفيذ fsync لكل تغيير. هذا مقبول للكتابات الفردية، لكنه بطيء عند التحميل بالجملة، كما أنه يمنعك من التراجع عن مجموعة من العبارات كوحدة واحدة. الأمر BEGIN يُعطّل وضع autocommit إلى أن تُنفّذ COMMIT أو ROLLBACK.

ROLLBACK: وكأن شيئاً لم يكن

الأمر ROLLBACK يتخلّص من كل ما جرى منذ الـ BEGIN المقابل، وتعود قاعدة البيانات إلى حالتها قبل بدء المعاملة.

يختفي كلٌّ من UPDATE و DELETE تمامًا، ويعود الجدول إلى حالته السابقة قبل BEGIN. هذه هي شبكة الأمان التي تسمح للتطبيق بالتراجع بشكل نظيف عند مواجهة خطأ في منتصف عملية متعددة الجمل.

وللعلم، انتهاك أي قيد (constraint) داخل المعاملة لا يؤدي تلقائيًا إلى التراجع عن المعاملة بأكملها. ما يحدث هو التراجع عن الجملة المخالفة فقط، مع إبقاء المعاملة مفتوحة بانتظار قرارك. فإذا أردت مبدأ "الكل أو لا شيء"، فعلى التطبيق أن يُصدر أمر ROLLBACK بنفسه عند اكتشاف الخطأ.

تسريع عمليات الإدخال الجماعي

بما أن كل جملة في وضع autocommit تُنفِّذ عملية fsync خاصة بها، فإن تجميع دفعة كاملة داخل معاملة واحدة قد يجعلها أسرع بمئة ضعف في الغالب:

عملية مزامنة واحدة مع القرص عند COMMIT بدلاً من واحدة لكل صف. إذا صادفت يوماً استيراد آلاف الصفوف ولاحظت أن العملية بطيئة بشكل غريب، فهذا هو السبب في الغالب.

الفرق بين DEFERRED و IMMEDIATE و EXCLUSIVE

تقبل BEGIN وسيطاً يحدد متى يقوم SQLite بأخذ الأقفال:

  • BEGIN DEFERRED (الوضع الافتراضي) — لا يوجد أي قفل إطلاقاً حتى تقرأ أو تكتب فعلياً. يُؤخذ قفل الكتابة بشكل كسول عند أول جملة كتابة.
  • BEGIN IMMEDIATE — يحجز قفل الكتابة فوراً. تستطيع الاتصالات الأخرى أن تقرأ، لكن لا يمكن لأي اتصال آخر أن يبدأ الكتابة.
  • BEGIN EXCLUSIVE — مثل IMMEDIATE، مع منع الاتصالات الأخرى من القراءة أيضاً. في وضع WAL يتصرف هذا الخيار تماماً مثل IMMEDIATE؛ والفرق يظهر فقط في وضع rollback journal القديم.
BEGIN DEFERRED;     -- مماثل لـ BEGIN العادي
BEGIN IMMEDIATE;    -- احجز قفل الكتابة الآن
BEGIN EXCLUSIVE;    -- احجز كل شيء (وضع سجل التراجع)

الاختيار هنا مهم جدًا في سياق التزامن (concurrency). لو استخدمت BEGIN العادية، يقدر اتصالان يبدآن transaction في نفس الوقت، ويقرآن من الجدول بدون مشاكل، ثم يحدث التصادم عندما يحاول كلاهما الكتابة — الاتصال الثاني الذي يطلب قفل الكتابة سيحصل على SQLITE_BUSY، والأسوأ أنه قام بقراءات لا بد أن يتخلى عنها الآن.

وهنا يأتي دور BEGIN IMMEDIATE: إذا كنت تعرف مسبقًا أنك ستكتب، فاطلب قفل الكتابة من البداية. الاتصال الثاني سيُحجب فورًا (أو يفشل سريعًا) قبل أن يبدأ أي عمل سيضطر للتخلي عنه لاحقًا.

قاعدة عامة: إذا كانت معاملتك ستتضمن كتابة، استخدم BEGIN IMMEDIATE.

القراءة داخل المعاملة ترى لقطة (snapshot) ثابتة

طوال فترة فتح المعاملة، قراءاتك ترى لقطة متسقة من قاعدة البيانات كما كانت لحظة بدء المعاملة (في وضع WAL) أو لحظة أول قراءة (في وضع rollback-journal). أي تغييرات يُثبّتها (commit) اتصال آخر لن تظهر فجأة في استعلاماتك.

أنت ترى تعديلاتك غير المُلتَزَم بها (uncommitted)، بينما لا يراها أي اتصال آخر. وبمجرد تنفيذ COMMIT، تصبح القيمة الجديدة مرئية للجميع. هذا بالضبط ما يقصده الناس حين يقولون إن SQLite يعمل بمستوى serializable — لا يوجد خيار اسمه READ COMMITTED لتفعّله، لأن الإعداد الافتراضي أصلاً هو أقوى مستوى عزل ممكن.

مثال على transaction في sqlite داخل كود التطبيق

في أي برنامج حقيقي، النمط الشائع هو وضع جسم العملية داخل try/except (أو try/catch)، مع استدعاء ROLLBACK في حال حدوث خطأ:

-- شيفرة وهمية لأي مكتبة عميل
BEGIN IMMEDIATE;
try:
    UPDATE accounts SET cents = cents - 2000 WHERE owner = 'Ada';
    UPDATE accounts SET cents = cents + 2000 WHERE owner = 'Boris';
    COMMIT;
except:
    ROLLBACK;
    raise;

معظم مكتبات العميل (مثل sqlite3 في بايثون و better-sqlite3 وغيرها) توفّر لك غلافاً جاهزاً عبر كتلة with أو دالة مساعدة اسمها transaction(). يستحق الأمر مراجعة توثيق المكتبة التي تستخدمها، لأن القيم الافتراضية ليست دائماً ما تتوقعه. مكتبة sqlite3 في بايثون تحديداً عُرفت تاريخياً بسلوك غريب في وضع autocommit، وقد أضافت الإصدارات الحديثة معاملاً جديداً اسمه autocommit لمعالجة هذه المشكلة.

أمور تُربك الكثيرين

  • أوامر DDL تعمل داخل المعاملات. يمكنك التراجع عن CREATE TABLE وALTER TABLE، بل حتى DROP TABLE. وهذا أمر مميّز في sqlite، فأغلب قواعد البيانات الأخرى تُثبّت أوامر DDL تلقائياً دون إمكانية التراجع.
  • VACUUM لا يعمل داخل معاملة. وكذلك بعض أوامر الصيانة الأخرى. شغّلها في وضع autocommit.
  • فشل COMMIT يعني فشلاً حقيقياً. إذا أعاد COMMIT خطأ SQLITE_BUSY (وهو أمر نادر لكنه ممكن)، فإن المعاملة لم تُثبَّت. على شيفرتك أن تتعامل مع هذه الحالة، وعادةً ما يكون الحل هو إعادة المحاولة.
  • المعاملات الطويلة تحجب كاتبين آخرين. المعاملة التي تبقى مفتوحة لدقائق ستحجب بقية الكتّاب لنفس المدة. افتحها متأخراً، وأغلقها بسرعة.

التالي: نقاط الحفظ (Savepoints)

أمرا BEGIN وCOMMIT يعملان بمنطق "كل شيء أو لا شيء". لكن أحياناً تحتاج إلى التراجع عن جزء من المعاملة فقط، كأن تتخلى عن خطوة محفوفة بالمخاطر مع الإبقاء على البقية. هذا بالضبط ما تقدّمه نقاط الحفظ، وهي موضوعنا التالي.

الأسئلة الشائعة

كيف أبدأ معاملة (transaction) في SQLite؟

نفّذ BEGIN; (أو BEGIN TRANSACTION;)، ثم اكتب أوامرك، وبعدها استخدم COMMIT; لحفظ التغييرات أو ROLLBACK; للتراجع عنها. وبدون BEGIN صريح، فإن كل أمر يُنفَّذ داخل معاملته الخاصة في وضع الـ autocommit.

ما الفرق بين BEGIN وBEGIN IMMEDIATE وBEGIN EXCLUSIVE؟

BEGIN (وهو نفسه BEGIN DEFERRED) لا يأخذ قفل الكتابة إلا عند أول عملية كتابة فعلية، وقد يفشل لاحقاً بخطأ SQLITE_BUSY لو سبقك أحد إليه. أما BEGIN IMMEDIATE فيحجز قفل الكتابة من البداية. وBEGIN EXCLUSIVE يذهب أبعد من ذلك ويمنع القرّاء الآخرين أيضاً (وهذا له معنى فقط خارج وضع WAL).

هل يدعم SQLite مستويات العزل (isolation levels)؟

ليس بالمعنى المعياري لـ SQL. عملياً، يعمل SQLite على مستوى SERIALIZABLE: كل معاملة ترى لقطة (snapshot) متّسقة من البيانات، والكتابات تُسلسَل واحدة تلو الأخرى. لا يوجد خيار READ COMMITTED ولا REPEATABLE READ؛ كل ما تتحكم فيه هو DEFERRED أو IMMEDIATE أو EXCLUSIVE، وهذه تحدّد متى تُؤخذ الأقفال، لا ما الذي يمكنك رؤيته.

هل يدعم SQLite المعاملات المتداخلة (nested transactions)؟

ليس مباشرة، فلا يمكنك استدعاء BEGIN داخل BEGIN آخر. لتحقيق التداخل استخدم SAVEPOINT مع RELEASE أو ROLLBACK TO، فهي تتيح لك تراجعاً جزئياً داخل معاملة واحدة. سنغطّي هذا في الصفحة التالية.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن