NestJS — Node.js للمؤسسات

المعاملات وسلامة البيانات

17 دقيقة الدرس 23 من 48

المعاملات وسلامة البيانات

بعض العمليات تمسّ عدّة صفوف أو جداول ويجب أن تنجح كلّها أو تفشل كلّها — لا نصفها أبدًا. تحويل المال يخصم من حساب ويُضيف لآخر؛ إن فشلت الكتابة الثانية، يجب التراجع عن الأولى. وتضمن المعاملة (transaction) سلوك الكل-أو-لا-شيء هذا.

المثال الكلاسيكي

تأمّل تحويلًا مصرفيًا. دون معاملة، يترك انهيارٌ بين التحديثين المالَ مخصومًا من حساب دون إضافته للآخر — بيانات فاسدة:

// خطير دون معاملة await this.accounts.decrement({ id: from }, 'balance', amount); // إن انهارت العملية هنا، اختفى المال await this.accounts.increment({ id: to }, 'balance', amount);

ACID باختصار

توفّر المعاملات ضمانات ACID: الذرّية (Atomicity) (الكل أو لا شيء)، والاتساق (Consistency) (من حالة صالحة إلى صالحة)، والعزل (Isolation) (المعاملات المتزامنة لا تُفسِد بعضها)، والمتانة (Durability) (البيانات المُثبَّتة تنجو من الانهيارات).

المعاملات بـ DataSource

أبسط نهج في TypeORM هو dataSource.transaction(). كل ما داخل ردّ النداء يُثبَّت معًا، أو يتراجع كلّيًا إن رُمي أي خطأ:

import { DataSource } from 'typeorm'; @Injectable() export class TransferService { constructor(private dataSource: DataSource) {} async transfer(from: number, to: number, amount: number) { await this.dataSource.transaction(async (manager) => { await manager.decrement(Account, { id: from }, 'balance', amount); await manager.increment(Account, { id: to }, 'balance', amount); // ارمِ خطأ في أي مكان هنا -> يتراجع كلا التغييرين تلقائيًا }); } }
استخدم manager المعاملة، لا مستودعاتك. داخل ردّ النداء، يجب أن تمرّ كل القراءات والكتابات عبر manager المُمرَّر (أو مُشغّل استعلام) لتتشارك المعاملة نفسها. استدعاء مستودع محقون عادي سيعمل خارج المعاملة.

نهج QueryRunner

لتحكّم أدقّ — تثبيت/تراجع صريح، أو حين تحتاج تحرير الاتصال بنفسك — استخدم مُشغّل استعلام:

const runner = this.dataSource.createQueryRunner(); await runner.connect(); await runner.startTransaction(); try { await runner.manager.save(orderEntity); await runner.manager.save(paymentEntity); await runner.commitTransaction(); } catch (err) { await runner.rollbackTransaction(); throw err; } finally { await runner.release(); // حرّر الاتصال دائمًا }
حرّر مُشغّل الاستعلام دائمًا. يحجز مُشغّل الاستعلام اتصالًا من المجمّع. نسيان release() (استخدم كتلة finally) يُسرّب الاتصالات حتى يُستنزَف المجمّع ويتوقّف التطبيق عن الاستجابة.

أبقِ المعاملات قصيرة

افعل عمل قاعدة البيانات فقط داخل معاملة. لا تستدعِ واجهة API خارجية بطيئة أو ترسل بريدًا داخلها — فهي تحجز الأقفال طوال الوقت، ما يضرّ التزامن. افعل العمل الخارجي قبلها أو بعدها، وأبقِ الكتلة المعامِلة مركّزة على الكتابات التي يجب أن تكون ذرّية.

الخلاصة

تجعل المعاملات تغييرات قاعدة البيانات متعدّدة الخطوات ذرّية — تُثبَّت كلّها أو تتراجع كلّها — حافظةً سلامة البيانات (ضمانات ACID). استخدم dataSource.transaction() للحالة الشائعة (اعمل عبر manager الخاص بها)، أو QueryRunner للتحكّم الصريح (حرّر دائمًا في finally). أبقِ المعاملات قصيرة وخالية من الاستدعاءات الخارجية. تاليًا: ORM مختلف، Prisma.