لا ينشئ 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
كيف تختار الحد المناسب للدالة المساعدة؟
اتبع تسلسل القرارات هذا عند مراجعة المستدعي ودوال المقالات المساعدة:
- يجب أن تنجح جميع العمليات معًا: دع المستدعي يتولى المعاملة ويمرر عميله إلى الدوال المساعدة التي تنفذ الاستعلامات فقط.
- يمكن التخلص من دفعة واحدة مع مواصلة المستدعي: أنشئ نقطة حفظ قبل تلك الدفعة، ولا تستعد إلا من الإخفاقات المقبولة، وأعِد النتيجة صراحةً.
- يجب تنفيذ COMMIT للدفعة بشكل مستقل: استخدم عميلًا ومعاملة منفصلين. عندها يظل COMMIT الخاص بها قائمًا بعد التراجع عن المعاملة الخارجية، ولذلك لا يمكنها تلبية المهمة الأصلية التي تتطلب نجاح الكل أو فشله معًا.
- ينتج عن تنفيذ متزامن إخفاق تسلسل: أعد تشغيل المعاملة كاملة بدلًا من التعامل مع الدالة المساعدة بوصفها حدًا مستقلًا لإعادة المحاولة.
تستند هذه الخيارات إلى دلالات نقاط الحفظ في PostgreSQL واشتراط node-postgres بقاء المعاملة على عميل واحد. PostgreSQL: SAVEPOINT، node-postgres: Transactions، PostgreSQL: Transaction Isolation
في مهمة تحرير المقال المعروضة هنا، أبقِ مسؤولية المعاملة لدى جهة واحدة، ومرّر عميلًا واحدًا عبر سلسلة الاستدعاءات، واستخدم نقطة حفظ للعمل الاختياري. راجع أوامر المعاملات في كل دالة مساعدة: إذ يغيّر COMMIT غير المتوقع حدود المعاملة للاتصال كله، بينما يظل المستدعي مسؤولًا عن النتيجة النهائية حتى بعد تحرير نقطة حفظ.
المصادر
BEGIN — مجموعة تطوير PostgreSQL العالمية
COMMIT — مجموعة تطوير PostgreSQL العالمية
3.4. Transactions — مجموعة تطوير PostgreSQL العالمية
psql — مجموعة تطوير PostgreSQL العالمية
CREATE TABLE — مجموعة تطوير PostgreSQL العالمية
SAVEPOINT — مجموعة تطوير PostgreSQL العالمية
RELEASE SAVEPOINT — مجموعة تطوير PostgreSQL العالمية
ROLLBACK TO SAVEPOINT — مجموعة تطوير PostgreSQL العالمية
SET TRANSACTION — مجموعة تطوير PostgreSQL العالمية
13.2. Transaction Isolation — مجموعة تطوير PostgreSQL العالمية
Appendix A. PostgreSQL Error Codes — مجموعة تطوير PostgreSQL العالمية
Transactions — node-postgres
Pool — node-postgres
Welcome — node-postgres
Connecting — node-postgres
Queries — node-postgres