لا ينشئ PostgreSQL معاملة متداخلة عند إرسال BEGIN مرة ثانية. بل يصدر تحذيرًا ويُبقي المعاملة الحالية؛ وينهيها أول COMMIT ناجح. استخدم نقاط الحفظ عندما تحتاج دالة مساعدة إلى التراجع الجزئي. PostgreSQL: BEGIN

يساعد هذا الدليل مطورًا يعمل في Mac Terminal على تشخيص دالة مساعدة تُنهي عملية تحرير مقال مبكرًا عبر تنفيذ COMMIT. تستخدم الأمثلة صفوف مقالات اصطناعية، وتوضح كيفية إبقاء معاملة واحدة تحت مسؤولية جهة واحدة، مع السماح لدالة مساعدة بالتراجع عن دفعتها الخاصة. وهي تستند إلى وثائق PostgreSQL وnode-postgres الحالية.

ماذا يحدث عندما تظهر BEGIN وCOMMIT داخل معاملة أخرى؟

تعمل أوامر معاملات SQL على المعاملة الحالية للاتصال. لا يحصي PostgreSQL استدعاءات BEGIN المتكررة منتظرًا استدعاءات COMMIT المطابقة. لا يؤدي BEGIN ثانٍ إلى تغيير المعاملة؛ أما COMMIT عادي وناجح فينفّذ COMMIT للمعاملة الحالية. ويؤدي إصدار COMMIT عادي آخر بعد ذلك إلى تحذير لعدم وجود معاملة مفتوحة. PostgreSQL: BEGIN، PostgreSQL: COMMIT

في معاملة سليمة، يكون تسلسل الحدود كالتالي:

الأمر المرسل عبر اتصال واحد الحد الناتج
أول BEGIN يفتح المعاملة
إدراج المقال الأول ينتمي إلى تلك المعاملة
ثاني BEGIN يصدر تحذيرًا؛ ولا ينشئ حدًا داخليًا
إدراج المقال الثاني ينتمي إلى المعاملة نفسها
أول COMMIT ينفّذ COMMIT لكلا الإدراجين وينهي المعاملة
عبارة أخرى تُرسل منفصلة تُنفّذ ضمن معاملتها الخاصة وفق سلوك التنفيذ التلقائي المعتاد
ROLLBACK لاحقًا لا يمكنه التراجع عن الإدراجات التي نُفّذ لها COMMIT بالفعل

يوضح هذا سببًا شائعًا لفشل التطبيقات: يفتح المستدعي معاملة، ثم يستدعي دالة مساعدة ترسل BEGIN وCOMMIT خاصين بها. وينهي COMMIT الصادر عن الدالة المساعدة معاملة المستدعي أيضًا. ولا يستطيع استثناء لاحق لدى المستدعي استعادة العملية الأصلية التي كان يُفترض أن تتم كلها أو لا تتم. وهذا الاستنتاج مستند إلى حدود المعاملات الموثقة في PostgreSQL. PostgreSQL: Transactions

أعِد إنتاج COMMIT المبكر في Mac Terminal

تحتاج إلى قاعدة بيانات PostgreSQL قيد التشغيل وأمر psql مثبّت مع إعدادات اتصال صالحة. افتح Terminal وابدأ جلسة جديدة باستخدام psql -X. يتخطى الخيار -X ملفات بدء التشغيل، ما يجعل هذا العرض أقل اعتمادًا على التخصيصات المحلية. شغّل النص البرمجي التالي في تلك الجلسة؛ فهو يفعّل التنفيذ التلقائي صراحةً. PostgreSQL: psql

الجدول مؤقت ولا يحتوي إلا على عناوين مقالات اصطناعية. يحافظ ON COMMIT PRESERVE ROWS على صفوفه لتظل متاحة للفحص عبر حدود المعاملات؛ ويحذف PostgreSQL الجدول المؤقت عند انتهاء الجلسة. يوضح هذا سلوك التراجع داخل الجلسة، لا التخزين الدائم بعد قطع الاتصال. PostgreSQL: CREATE TABLE

\set AUTOCOMMIT on

CREATE TEMP TABLE article_boundary_demo (
    id integer PRIMARY KEY,
    title text NOT NULL
) ON COMMIT PRESERVE ROWS;

BEGIN;
INSERT INTO article_boundary_demo VALUES (1, 'Opening draft');

-- Deliberately incorrect attempt to open an inner transaction.
BEGIN;
INSERT INTO article_boundary_demo VALUES (2, 'Middle draft');
COMMIT;

-- The previous COMMIT has already ended the transaction.
INSERT INTO article_boundary_demo VALUES (3, 'Closing draft');
ROLLBACK;

SELECT id, title FROM article_boundary_demo ORDER BY id;

تحتوي النتيجة المتوقعة على الصفوف الثلاثة. يصدر BEGIN الثاني تحذيرًا بأن معاملة قيد التنفيذ بالفعل. وينفّذ COMMIT أمر COMMIT للصفين 1 و2؛ ثم يُنفّذ COMMIT للصف 3 عبر التنفيذ التلقائي. ولا توجد معاملة مفتوحة كي يتراجع عنها ROLLBACK الأخير. هذه نتائج متوقعة مستنبطة من السلوك الموثق، وليست ناتجة عن تنفيذ مُبلّغ عنه لقاعدة بيانات. PostgreSQL: BEGIN، PostgreSQL: Transactions

أبقِ هذا التكرار في psql التفاعلي كما هو موضح. فطريقة إرسال الأوامر من العميل مهمة: قد تشترك عدة عبارات SQL تُمرر معًا عبر سلسلة واحدة إلى psql -c في معاملة ضمنية، ما لم تفصل بينها أوامر معاملات صريحة. وتوصي الوثائق باستخدام أوامر منفصلة أو الإدخال القياسي عندما يكون المقصود تنفيذها بشكل منفصل. PostgreSQL: psql

كيف تحافظ نقاط الحفظ على المعاملة الخارجية؟

تحدد نقطة الحفظ موضعًا داخل معاملة قائمة. يتخلص ROLLBACK TO SAVEPOINT من التغييرات التي تلي ذلك الموضع مع الإبقاء على العمل السابق. أما RELEASE SAVEPOINT فيزيل العلامة ويحتفظ بالتغييرات المتبقية ضمن المعاملة الحاوية. وهو لا ينفّذ COMMIT لهذه التغييرات بشكل مستقل. PostgreSQL: SAVEPOINT، PostgreSQL: RELEASE SAVEPOINT

شغّل هذا المثال الكامل في الجلسة نفسها أو في جلسة psql جديدة أخرى. فهو ينشئ جدولًا مؤقتًا خاصًا به:

CREATE TEMP TABLE article_savepoint_demo (
    id integer PRIMARY KEY,
    title text NOT NULL
) ON COMMIT PRESERVE ROWS;

BEGIN;
INSERT INTO article_savepoint_demo VALUES (1, 'Opening draft');

SAVEPOINT optional_article;
INSERT INTO article_savepoint_demo VALUES (2, 'Optional draft');
ROLLBACK TO SAVEPOINT optional_article;
RELEASE SAVEPOINT optional_article;

INSERT INTO article_savepoint_demo VALUES (3, 'Closing draft');
COMMIT;

SELECT id, title FROM article_savepoint_demo ORDER BY id;

الصفوف المتوقعة هي 1 و3. يُتخلص من الصف 2 عند نقطة الحفظ، بينما يظل صفا البداية والنهاية جزءًا من معاملة واحدة حتى COMMIT النهائي. وإذا استبدلت COMMIT النهائي بـROLLBACK، فسيُتخلص من كلا الإدراجين المتبقيين. PostgreSQL: SAVEPOINT، PostgreSQL: Transactions

يهم أيضًا عمر نقطة الحفظ عند تركيب الدوال المساعدة. فالتراجع إلى نقطة حفظ يترك نقطة الحفظ المسماة متاحة، لكنه يزيل نقاط الحفظ اللاحقة. ويزيل تحرير نقطة الحفظ تلك النقطة وأي نقاط حفظ نشطة أُنشئت بعدها. يحرر المثال علامته صراحةً بعد الاستعادة. PostgreSQL: ROLLBACK TO SAVEPOINT، PostgreSQL: RELEASE SAVEPOINT

كيف ينبغي لدوال node-postgres المساعدة مشاركة المعاملة؟

يشترط node-postgres استخدام نسخة العميل نفسها لكل عبارة ضمن المعاملة، ولا يوفر تجريدًا تلقائيًا للتداخل بين المعاملات. مرّر إلى الدوال المساعدة عميل المستدعي. وإذا كان التطبيق يستخدم تجمعًا، فاحصل على عميل عبر pool.connect()، واستخدم ذلك العميل في كل العمليات، ثم حرره بعد ذلك؛ إذ يمكن لـpool.query() توجيه الاستعلامات المتعاقبة إلى عملاء مختلفين. node-postgres: Transactions، node-postgres: Pool

يستخدم البرنامج الكامل التالي نسخة Client مباشرة واحدة. ويتولى المستدعي مسؤولية BEGIN وCOMMIT وROLLBACK كاملة. وتحيط الدالة المساعدة بدفعة اختيارية من صفين بنقطة حفظ. ويكرر الإدراج الثاني عمدًا معرّف المقال 1، ما يتسبب في خطأ تفرد؛ فتتخلص عملية الاستعادة من الدفعة الاختيارية كلها، بما فيها الإدراج السابق الذي نجح.

إعداد المثال الكامل وتشغيله

استخدم إصدارًا مدعومًا حاليًا من Node.js مع تثبيت npm. شغّل npm install pg في مجلد عمل، ثم احفظ البرنامج أدناه باسم article-transactions.mjs. ويستخدم إعدادات اتصال PostgreSQL الموجودة لديك، بما فيها PGHOST وPGPORT وPGDATABASE وPGUSER متى كانت مُعدّة. يجب أن تشير هذه الإعدادات إلى قاعدة بيانات صالحة قبل تشغيل المثال. node-postgres: Welcome، node-postgres: Connecting

import pg from 'pg'
const { Client } = pg

async function addOptionalArticles(client) {
  await client.query('SAVEPOINT optional_articles')
  try {
    await client.query(
      'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
      [2, 'Optional draft']
    )
    await client.query(
      'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
      [1, 'Duplicate draft']
    )
  } catch (error) {
    if (error.code !== '23505') throw error
    await client.query('ROLLBACK TO SAVEPOINT optional_articles')
    await client.query('RELEASE SAVEPOINT optional_articles')
    return false
  }
  await client.query('RELEASE SAVEPOINT optional_articles')
  return true
}

async function main() {
  const client = new Client({ connectionTimeoutMillis: 5000 })
  await client.connect()
  try {
    await client.query(`
      CREATE TEMP TABLE article_demo (
        id integer PRIMARY KEY,
        title text NOT NULL
      ) ON COMMIT PRESERVE ROWS
    `)
    await client.query('BEGIN ISOLATION LEVEL READ COMMITTED')
    try {
      await client.query(
        'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
        [1, 'Opening draft']
      )
      const optionalKept = await addOptionalArticles(client)
      console.log('Optional batch kept:', optionalKept)
      await client.query(
        'INSERT INTO article_demo (id, title) VALUES ($1, $2)',
        [3, 'Closing draft']
      )
      await client.query('COMMIT')
    } catch (error) {
      try {
        await client.query('ROLLBACK')
      } catch (rollbackError) {
        throw new AggregateError(
          [error, rollbackError], 'Transaction and rollback failed'
        )
      }
      throw error
    }
    const result = await client.query(
      'SELECT id, title FROM article_demo ORDER BY id'
    )
    console.table(result.rows)
  } finally {
    await client.end()
  }
}

main().catch(error => {
  console.error(error)
  process.exitCode = 1
})

شغّل node article-transactions.mjs. ويعرض الإخراج المتوقع Optional batch kept: false ومعرّفي المقال 1 و3. وتستخدم القيم واجهة المعاملات query(text, values) الموثقة. جرى التحقق من صحة صياغة JavaScript باستخدام Node.js؛ وفُحص سيناريو قاعدة البيانات بالرجوع إلى الوثائق، لكنه لم يُنفّذ هنا. node-postgres: Queries

لا تستعد إلا من إخفاق مقبول صراحةً

يُسنِد PostgreSQL الرمز 23505 إلى unique_violation، ويوصي بالتحقق من رموز الأخطاء بدلًا من نص الرسالة. هنا، يُعد قبول ذلك الرمز قاعدة توضيحية للدفعة الاصطناعية الاختيارية. وتنتقل الأخطاء الأخرى إلى المستدعي، الذي يحاول التراجع عن المعاملة بأكملها. وتعيد الدالة المساعدة نتيجة صريحة ليكون العمل الاختياري الذي جرى التخلص منه ظاهرًا للمستدعي. PostgreSQL: PostgreSQL Error Codes

إن التقاط استثناء JavaScript وحده لا يعيد معاملة PostgreSQL الفاشلة إلى حالتها السابقة. يتطلب الاسترداد التراجع إلى نقطة حفظ أُنشئت مسبقًا أو التراجع الكامل. وعلى وجه الخصوص، لا يمكن استعادة معاملة أُجهضت بمجرد تحرير نقطة حفظها. PostgreSQL: Transactions، PostgreSQL: RELEASE SAVEPOINT

تفترض الدالة المساعدة وجود معاملة نشطة واستخدام هذا العميل بالتتابع. وهي ليست غلافًا عامًا للمعاملات العودية؛ إذ يتطلب تجريد تداخل قابلًا لإعادة الاستخدام إدارة صريحة للملكية وأسماء نقاط الحفظ أيضًا.

ما حدود العزل المتبقية بعد إصلاح الحدود؟

توفر نقاط الحفظ تراجعًا جزئيًا ضمن المعاملة الحالية؛ لكنها لا تحدد سياسة عزل منفصلة. اختر مستوى العزل عند الحد الخارجي. لا يسمح PostgreSQL بتغييره بعد أول استعلام أو عبارة تعديل بيانات في المعاملة، لذا لا تستطيع دالة مساعدة رفع مستوى العزل في معاملة سبق استخدامها بإرسال BEGIN آخر. PostgreSQL: SAVEPOINT، PostgreSQL: SET TRANSACTION

في الاستعلامات العادية، احتفظ بهذه الفروق:

مستوى العزل القيد ذو الصلة
READ COMMITTED قد ترى الاستعلامات المتعاقبة تغييرات نُفّذ لها COMMIT حديثًا من معاملات أخرى. وجود معاملة واحدة لا يعني وجود لقطة ثابتة واحدة.
REPEATABLE READ تشترك الاستعلامات في اللقطة التي أُنشئت عند أول عبارة ليست للتحكم بالمعاملة، مع رؤية تغييراتها السابقة. وتظل إخفاقات التسلسل ممكنة.
SERIALIZABLE يكون تأثير المعاملات التي نُفّذ لها COMMIT بنجاح متسقًا مع ترتيب تسلسلي ما. ومع ذلك، يجب على التطبيقات التعامل مع إخفاقات التسلسل.
READ UNCOMMITTED يتعامل PostgreSQL معه على أنه READ COMMITTED.

PostgreSQL: Transaction Isolation، PostgreSQL: SET TRANSACTION

يتطلب إخفاق التسلسل إعادة محاولة المعاملة كاملة، بما يشمل القرارات التي أنتجت أوامر SQL الخاصة بها. إعادة محاولة عمل نقطة الحفظ في الدالة المساعدة وحده لا تحدّث لقطة المعاملة الخارجية. ويحدد PostgreSQL إخفاقات التسلسل برمز SQLSTATE 40001. ينقل البرنامج التوضيحي هذه الإخفاقات إلى المستدعي؛ ولا ينفذ إعادة المحاولة تلقائيًا. PostgreSQL: Transaction Isolation

كيف تختار الحد المناسب للدالة المساعدة؟

اتبع تسلسل القرارات هذا عند مراجعة المستدعي ودوال المقالات المساعدة:

  1. يجب أن تنجح جميع العمليات معًا: دع المستدعي يتولى المعاملة ويمرر عميله إلى الدوال المساعدة التي تنفذ الاستعلامات فقط.
  2. يمكن التخلص من دفعة واحدة مع مواصلة المستدعي: أنشئ نقطة حفظ قبل تلك الدفعة، ولا تستعد إلا من الإخفاقات المقبولة، وأعِد النتيجة صراحةً.
  3. يجب تنفيذ COMMIT للدفعة بشكل مستقل: استخدم عميلًا ومعاملة منفصلين. عندها يظل COMMIT الخاص بها قائمًا بعد التراجع عن المعاملة الخارجية، ولذلك لا يمكنها تلبية المهمة الأصلية التي تتطلب نجاح الكل أو فشله معًا.
  4. ينتج عن تنفيذ متزامن إخفاق تسلسل: أعد تشغيل المعاملة كاملة بدلًا من التعامل مع الدالة المساعدة بوصفها حدًا مستقلًا لإعادة المحاولة.

تستند هذه الخيارات إلى دلالات نقاط الحفظ في PostgreSQL واشتراط node-postgres بقاء المعاملة على عميل واحد. PostgreSQL: SAVEPOINT، node-postgres: Transactions، PostgreSQL: Transaction Isolation

في مهمة تحرير المقال المعروضة هنا، أبقِ مسؤولية المعاملة لدى جهة واحدة، ومرّر عميلًا واحدًا عبر سلسلة الاستدعاءات، واستخدم نقطة حفظ للعمل الاختياري. راجع أوامر المعاملات في كل دالة مساعدة: إذ يغيّر COMMIT غير المتوقع حدود المعاملة للاتصال كله، بينما يظل المستدعي مسؤولًا عن النتيجة النهائية حتى بعد تحرير نقطة حفظ.

المصادر