لإبقاء معاملة node-postgres متماسكة، احصل على عميل واحد باستخدام pool.connect()، ونفّذ كل عبارة من خلال ذلك العميل، ثم أطلقه بعد الإتمام أو التراجع. مرّر ذلك العميل أيضًا إلى الدوال المساعدة. يشترط دليل المعاملات الرسمي اتباع هذا النمط.
هذا البرنامج التعليمي موجّه إلى مطوّر يستخدم Terminal على جهاز Mac لإجراء عدة تغييرات على صفوف المقالات بحيث تنجح جميعها أو تفشل معًا. ويتضمن برنامجًا كاملًا بصفوف مقالات اصطناعية، واختبارًا متعمدًا للتراجع، وتسلسلًا تشخيصيًا للاستعلامات المخفية داخل الدوال المساعدة.
لماذا يؤدي pool.query() إلى كسر نطاق المعاملة؟
تدير مجموعة الاتصالات الاتصالات، لكنها لا تجعل كل استعلام جزءًا من معاملة واحدة. كل استدعاء لـ pool.query() يحصل داخليًا على عميل متاح. لذلك لا يوجد ضمان بأن الاستدعاءات المتتالية ستستخدم الاتصال نفسه. وتحذّر واجهة Pool البرمجية في node-postgres صراحةً من استخدام هذه الطريقة للمعاملات.
فعلى سبيل المثال، إن إرسال BEGIN عبر مجموعة الاتصالات ثم إرسال عملية إدراج عبرها لا يضمن أن تكون عملية الإدراج ضمن المعاملة التي بدأتها. تنفّذ PostgreSQL عادةً العبارات الواقعة خارج كتلة معاملة صريحة ضمن معاملات مستقلة خاصة بها. ولا يمكن لتراجع لاحق على اتصال آخر إلغاء تلك التغييرات التي تم تثبيتها بشكل مستقل. راجع توثيق BEGIN في PostgreSQL.
الحد الفاصل المفيد هو كائن العميل الذي تم استعارته من المجموعة. لا يحدد اسم الدالة المساعدة أو الوحدة التي تحتويها، ولا حقيقة أن الدالة المستدعية تنتظرها، ما إذا كانت ضمن المعاملة. تشارك الدالة المساعدة عندما تستخدم استعلاماتها العميل الذي بدأ المعاملة. وهذا نابع مباشرةً من شرط استخدام العميل نفسه الوارد في دليل المعاملات.
شغّل مثالًا كاملًا للإتمام والتراجع على جهاز Mac
تحتاج إلى Node.js مع npm، وخادم PostgreSQL قيد التشغيل، وقاعدة بيانات تطوير محلية موجودة. يفترض هذا الشرح أن اسم قاعدة البيانات هو article_demo وأن إعدادات الاتصال المحلية المعتادة تعمل بالفعل. تثبيت قاعدة البيانات خارج نطاق مهمة المعاملة هذه.
في Terminal، أنشئ مجلد عمل جديدًا وثبّت برنامج التشغيل:
mkdir pg-transaction-demo
cd pg-transaction-demo
npm init -y
npm install pg
احفظ البرنامج الكامل التالي باسم transaction-demo.mjs. يحتوي جدوله المؤقت على معرّفات وعناوين مقالات اصطناعية فقط. تبقى جداول PostgreSQL المؤقتة عادةً بعد الإتمام، وتُحذف عند انتهاء الجلسة؛ وتتيح هاتان الخاصيتان للبرنامج فحص دفعة ناجحة بعد التراجع عن دفعة ثانية. راجع CREATE TABLE.
يتبع مساعد المعاملة أدناه التسلسل الموثّق BEGIN وCOMMIT وROLLBACK. ويضيف معالجة صريحة لحالة فشل التراجع أيضًا، مع الحفاظ على الخطأين بدلًا من استبدال الخطأ الأصلي. تستخدم قيم الاستعلام واجهة النص والقيم الموثّقة ضمن الاستعلامات.
import pg from 'pg';
import assert from 'node:assert/strict';
const { Pool } = pg;
async function inTransaction(client, work) {
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
try {
const result = await work(client);
await client.query('COMMIT');
return result;
} catch (error) {
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
throw new AggregateError(
[error, rollbackError],
'Transaction failed; rollback also failed'
);
}
throw error;
}
}
async function insertArticle(client, article) {
const result = await client.query(
`INSERT INTO pg_temp.transaction_demo_articles (id, title)
VALUES ($1, $2)
RETURNING id, title`,
[article.id, article.title]
);
return result.rows[0];
}
async function insertBatch(client, articles) {
const inserted = [];
for (const article of articles) {
inserted.push(await insertArticle(client, article));
}
return inserted;
}
async function main() {
const pool = new Pool({ connectionTimeoutMillis: 5000 });
let client;
let discardClient = false;
pool.on('error', (error) => {
console.error('Idle client error:', error);
process.exitCode = 1;
});
try {
client = await pool.connect();
await client.query(`
CREATE TEMP TABLE transaction_demo_articles (
id text PRIMARY KEY,
title text NOT NULL
) ON COMMIT PRESERVE ROWS
`);
const saved = await inTransaction(client, (tx) =>
insertBatch(tx, [
{ id: 'article-001', title: 'A guide to paper shapes' },
{ id: 'article-002', title: 'Choosing a notebook layout' }
])
);
assert.equal(saved.length, 2);
await assert.rejects(
() => inTransaction(client, (tx) =>
insertBatch(tx, [
{ id: 'article-003', title: 'Arranging a reading desk' },
{ id: 'article-001', title: 'Deliberate duplicate ID' }
])
),
(error) => error.code === '23505'
);
const remaining = await client.query(`
SELECT id, title
FROM pg_temp.transaction_demo_articles
ORDER BY id
`);
assert.deepEqual(
remaining.rows.map((row) => row.id),
['article-001', 'article-002']
);
console.log('Commit and rollback checks passed.');
console.table(remaining.rows);
} catch (error) {
discardClient = true;
throw error;
} finally {
if (client) client.release(discardClient);
await pool.end();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
شغّله من المجلد نفسه:
PGDATABASE=article_demo node transaction-demo.mjs
تقرأ مجموعة الاتصالات متغيرات بيئة PostgreSQL الخاصة بالاتصال. إذا كان إعدادك المحلي يتطلب إعدادات اتصال مختلفة، فمرّر PGHOST وPGPORT المناسبين إلى جانب PGDATABASE. يشرح دليل الاتصال الرسمي هذه المتغيرات وقيمها الافتراضية.
عند نجاح التشغيل، توقّع ظهور Commit and rollback checks passed. متبوعًا بصفّي المقالتين الأصليين. تُدرج الدفعة الثانية article-003 ثم تحاول استخدام article-001 مجددًا. وينتج عن التكرار رمز حالة SQLSTATE 23505، الموثّق باسم unique_violation في معالجة إخفاق التسلسل. يؤدي التراجع عن الدفعة إلى إزالة عملية الإدراج السابقة فيها أيضًا؛ ويتحقق التأكيد من غياب article-003.
تم التحقق من مسارات التحكم الخاصة بالنجاح وخطأ العمل وخطأ التراجع في مساعد المعاملة باستخدام عميل وهمي حتمي. لم يُنفّذ مثال قاعدة البيانات هنا لعدم توفر حزمة pg واتصال PostgreSQL للاختبار في البيئة؛ والناتج أعلاه يصف النتيجة المتوقعة.
كيف ينبغي للدوال المساعدة مشاركة عميل المعاملة؟
استخدم معاملات عميل صريحة على امتداد سلسلة الاستدعاءات. في البرنامج، تحصل main() على العميل، وتمرّر inTransaction() ذلك الكائن نفسه إلى رد النداء، ثم تمرّره insertBatch() إلى insertArticle(). لا يوجد استحواذ على اتصال داخل أي من دالتي الإدراج المساعدتين.
في هذا التنفيذ، اجعل المسؤوليات واضحة:
| الدالة | المسؤولية |
|---|---|
main() |
الحصول على العميل، وتنفيذ إعداد العرض التوضيحي، وإطلاق العميل، وإغلاق مجموعة اتصالات البرنامج النصي. |
inTransaction() |
بدء معاملة واحدة وإنهاؤها على العميل الممرّر إليها. |
insertBatch() |
انتظار كل عملية مقالة وتمرير العميل الممرّر إليها. |
insertArticle() |
تنفيذ عملية الإدراج من خلال العميل الممرّر إليها. |
عقد الدالة المساعدة محدد عمدًا: لا تستدعِ inTransaction() إلا عندما لا تكون لدى ذلك العميل معاملة نشطة، ودع الإخفاقات تنتشر خارج رد النداء. إن استدعاء BEGIN داخل معاملة PostgreSQL قائمة لا ينشئ معاملة متداخلة مستقلة؛ وتوثّق PostgreSQL نقاط الحفظ بوصفها آلية العمل المتداخل. راجع BEGIN.
انتظر اكتمال كل عملية تنتمي إلى المعاملة قبل الرجوع من رد النداء. في هذا المثال، تجعل الحلقة التسلسلية نقطة الاكتمال واضحة. إن بدء عمليات إدراج غير متزامنة من دون إرجاع وعودها أو انتظارها سيتيح لرد النداء الانتهاء قبل أن يتلقى نتائجها كلها.
ماذا ينبغي أن يحدث عند فشل استعلام أو التراجع؟
يصل إخفاق الاستعلام العادي إلى كتلة catch في مساعد المعاملة، التي تحاول التراجع على العميل نفسه ثم تعيد إطلاق الخطأ الأصلي. ولا يُقبل التكرار المتعمد إلا بعد اكتمال التراجع. وإذا فشل التراجع، يطلق المساعد AggregateError؛ ولا يقبل فحص خطأ التكرار هذا الخطأ، لذا ينتقل التنفيذ إلى معالج الإخفاق الخارجي.
يتخلّص المثال بحذر من العميل بعد أي إخفاق غير متوقع باستخدام client.release(true). وفي المسار المعتاد، يعيد العميل باستخدام client.release(false). وسيطة الإتلاف موثّقة في واجهة Pool البرمجية.
أطلق دائمًا أي عميل تم الحصول عليه بنجاح. وإلا فقد ينتظر المستدعون اللاحقون اتصالًا لن يصبح متاحًا أبدًا. أغلق مجموعة الاتصالات بعد انتهاء هذا البرنامج المستقل؛ أما في التطبيق، فيجب أن يكون إغلاق المجموعة جزءًا من إغلاق التطبيق. يشرح دليل تجميع الاتصالات متطلبات الإطلاق وكيف ينتظر pool.end() عودة العملاء المستعارين.
هل يضمن عميل واحد لقطة مستقرة؟
استمرارية العميل ومستوى العزل يجيبان عن سؤالين مختلفين. يحدد العميل المعاملة التي تتلقى عباراتك. ويحدد مستوى العزل ما يمكن لتلك العبارات رؤيته أثناء تشغيل معاملات أخرى. يحدد توثيق عزل المعاملات في PostgreSQL هذه الحدود:
| مستوى العزل | الحد ذو الصلة |
|---|---|
READ COMMITTED |
المستوى الافتراضي. قد ترى القراءات العادية المتعاقبة بيانات مختلفة تم تثبيتها بالتزامن. تحصل كل عبارة على لقطة جديدة. |
REPEATABLE READ |
يستخدم لقطة أُنشئت عند أول عبارة ليست للتحكم بالمعاملة. تمنع PostgreSQL القراءات الشبحية، لكن تظل حالات شذوذ التسلسل ممكنة. |
SERIALIZABLE |
تتصرف المعاملات القابلة للتسلسل والمثبّتة باتساق مع تنفيذ تسلسلي ما؛ وقد تفشل معاملة وتحتاج إلى إعادة المحاولة. |
READ UNCOMMITTED |
يتصرف في PostgreSQL مثل READ COMMITTED. |
يمكن للقراءات أيضًا رؤية التغييرات التي أجرتها معاملتها في وقت سابق. لذلك، فإن رؤية عملية الإدراج الخاصة بك لا تعني أن كل القراءات اللاحقة ستملك رؤية ثابتة للتغييرات التي أجرتها معاملات أخرى. راجع عزل المعاملات.
يحدد المثال صراحةً READ COMMITTED في العبارة الافتتاحية. لاستخدام مستوى مختلف، غيّر تلك العبارة قبل تشغيل المعاملة. تتيح PostgreSQL تحديد خصائص المعاملة في BEGIN؛ وإذا استخدمت SET TRANSACTION بدلًا من ذلك، فيجب ضبط العزل قبل أول استعلام أو عبارة لتعديل البيانات. راجع BEGIN وSET TRANSACTION.
إذا اخترت REPEATABLE READ أو SERIALIZABLE، فاستعد لرمز حالة SQLSTATE 40001. أعد محاولة المعاملة كاملة، بما في ذلك المنطق الذي يحدد العبارات والقيم، بدلًا من إعادة تنفيذ الاستعلام الفاشل فقط. يتعمّد المثال إعادة إطلاق مثل هذه الأخطاء بدلًا من تنفيذ إعادة المحاولة. تشرح معالجة إخفاق التسلسل في توثيق PostgreSQL هذا الشرط وتشير إلى أن إعادة المحاولة قد تفشل مجددًا.
كيف يمكنك العثور على استعلام خرج عن المعاملة؟
تتبّع دفعة مقصودة واحدة من استحواذها على اتصال وحتى إتمامها أو التراجع عنها. استخدم هذا التسلسل لمراجعة شيفرتك:
- حدّد استدعاء
pool.connect()وتعرّف على العميل الذي يعيده. - تتبّع كل دالة مساعدة تُستدعى بين
BEGINواكتمال المعاملة. تحقّق من أن كل دالة تتلقى ذلك العميل وتستخدمه. - افحص أغلفة الاستعلامات المشتركة بحثًا عن استدعاءات
pool.query()مخفية أو عمليات استحواذ إضافية على الاتصالات. - تحقّق من انتظار كل عملية ومن أن الدوال المساعدة تمرّر الإخفاقات.
- كيّف اختبار الصف المكرر الاصطناعي ليتناسب مع دفعتك، وتحقق من اختفاء عملية إدراج سابقة بعد فشل عبارة لاحقة.
يقدم اختبار التراجع الناجح دليلًا مفيدًا للمسار الذي تم اختباره، لكنه لا يثبت أن كل مسارات التطبيق تستخدم العميل الصحيح. راجع الدوال المساعدة الشرطية أيضًا. وإذا بقيت الاستعلامات على عميل واحد مع استمرار اختلاف القراءات المتكررة، فقارن ذلك السلوك بمستوى العزل المحدد قبل تغيير طريقة التعامل مع الاتصالات.
اجعل الحصول على العميل وإطلاقه واضحين، ومرّر العميل صراحةً، واجعل اكتمال المعاملة مشروطًا بانتهاء جميع العمليات المقصودة. يمنحك ذلك بنية ملموسة لفحصها واختبار تراجع يمكنك تكراره مع نمو سير عمل المقالات.
المصادر
- المعاملات — node-postgres.
- pg.Pool — node-postgres.
- الاستعلامات — node-postgres.
- الاتصال — node-postgres.
- تجميع الاتصالات — node-postgres.
- BEGIN — مجموعة PostgreSQL للتطوير العالمي.
- CREATE TABLE — مجموعة PostgreSQL للتطوير العالمي.
- 13.2. عزل المعاملات — مجموعة PostgreSQL للتطوير العالمي.
- SET TRANSACTION — مجموعة PostgreSQL للتطوير العالمي.
- 13.5. معالجة إخفاق التسلسل — مجموعة PostgreSQL للتطوير العالمي.