انتقل إلى المحتوى الرئيسي

الفصل 2: إدارة المعاملات (@Transactional)

مقدمة​

تخيل تحويلًا مصرفيًا: تقوم بخصم 100€ من الحساب A وتضيف 100€ إلى الحساب B. ماذا يحدث إذا فشل الإيداع بعد الخصم؟ بدون معاملة، ستختفي 100€!

تضمن المعاملات أن تكون عمليات البيانات الخاصة بك موثوقة ومتسقة.


🎯 أهداف الفصل​

في نهاية هذا الفصل، ستعرف:

  1. ✅ فهم خصائص ACID
  2. ✅ استخدام @Transactional بشكل صحيح
  3. ✅ اختيار مستوى الانتشار المناسب
  4. ✅ فهم مستويات العزل
  5. ✅ إدارة التراجع بشكل صحيح
  6. ✅ تجنب الفخاخ الشائعة

1. ما هي المعاملة؟​

تعريف​

المعاملة هي مجموعة من العمليات التي يجب أن تنجح جميعها أو تفشل جميعها.

مثال عملي: تحويل مصرفي​

// بدون معاملة (خطر!)
public void transfert(Long compteA, Long compteB, BigDecimal montant) {
compteRepo.debiter(compteA, montant); // ← نجح: -100€

// 💥 خطأ هنا (شبكة، استثناء، إلخ.)

compteRepo.crediter(compteB, montant); // ← لم يتم تنفيذه أبدًا!

// النتيجة: 100€ اختفت! 💀
}
// مع معاملة (آمن)
@Transactional
public void transfert(Long compteA, Long compteB, BigDecimal montant) {
compteRepo.debiter(compteA, montant); // ← داخل المعاملة

// 💥 خطأ هنا

compteRepo.crediter(compteB, montant); // ← لم يتم تنفيذه أبدًا

// النتيجة: تراجع تلقائي، تم إلغاء الخصم ✓
}

2. خصائص ACID​

كل معاملة تلتزم بأربع خصائص أساسية:

A - الذرية (Atomicity)​

"الكل أو لا شيء"

✅ جميع العمليات تنجح → COMMIT
❌ عملية واحدة تفشل → ROLLBACK للجميع

C - التناسق (Consistency)​

"تبقى قاعدة البيانات في حالة صالحة"

قبل: الحساب A = 500€، الحساب B = 300€، المجموع = 800€
بعد: الحساب A = 400€، الحساب B = 400€، المجموع = 800€

المال لا يختفي ولا يظهر بشكل سحري.

I - العزل (Isolation)​

"المعاملات لا ترى بعضها البعض"

المعاملة 1: تقرأ الرصيد = 500€
المعاملة 2: تعدل الرصيد = 400€
المعاملة 1: تقرأ مرة أخرى = 500€ (أو 400€ حسب العزل)

D - المتانة (Durability)​

"بمجرد COMMIT، يكون نهائيًا"

المعاملة COMMIT ✓
→ حتى إذا تعطل الخادم بعد ذلك، يتم حفظ البيانات.

3. @Transactional بالتفصيل​

الاستخدام الأساسي​

import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

@Transactional
public void createOrder(Order order) {
// جميع هذه العمليات في معاملة واحدة
orderRepo.save(order);
inventoryService.decreaseStock(order.getItems());
paymentService.processPayment(order.getTotal());
emailService.sendConfirmation(order);
}
}

كيف يعمل؟​

يستخدم Spring وكيلًا حول فصلك:

أين تضع @Transactional؟​

// ✅ موصى به: على طريقة الخدمة
@Service
public class UserService {

@Transactional
public void createUser(User user) {
// ...
}
}

// ✅ حسنًا: على الفئة (جميع الطرق العامة)
@Service
@Transactional
public class UserService {

public void createUser(User user) { /* معاملة */ }
public void updateUser(User user) { /* معاملة */ }
}

// ❌ لا يعمل: على الطرق الخاصة
@Service
public class UserService {

@Transactional // ← تم تجاهله!
private void internalOperation() {
// الوكيل لا يمكنه اعتراض الطرق الخاصة
}
}

4. مستويات الانتشار​

يحدد الانتشار كيف تتصرف المعاملة عندما تستدعي طريقة أخرى تتعامل مع المعاملات.

REQUIRED (افتراضي)​

"يعيد استخدام المعاملة الحالية، أو ينشئ واحدة جديدة"

@Transactional(propagation = Propagation.REQUIRED)  // افتراضي
public void methodA() {
methodB(); // ← يعيد استخدام معاملة methodA
}

@Transactional
public void methodB() {
// نفس معاملة methodA
}
methodA() BEGIN TRANSACTION
├── عمليات methodA
├── methodB() [نفس المعاملة]
│ └── عمليات methodB
└── COMMIT (أو ROLLBACK شامل)

REQUIRES_NEW​

"ينشئ دائمًا معاملة جديدة"

@Transactional
public void methodA() {
// معاملة 1
repo.save(entity);

loggingService.logAction("action"); // ← معاملة 2 (مستقلة)

throw new RuntimeException(); // ← تراجع معاملة 1
// لكن السجل محفوظ (تمت معاملة 2)
}

@Service
public class LoggingService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String action) {
// معاملة جديدة مستقلة
logRepo.save(new Log(action));
}
}
methodA() BEGIN TRANSACTION 1
├── حفظ الكيان
├── logAction() BEGIN TRANSACTION 2
│ ├── حفظ السجل
│ └── COMMIT TRANSACTION 2 ✓
├── رمي استثناء
└── ROLLBACK TRANSACTION 1 (الكيان ملغى، السجل محفوظ)

أنواع الانتشار الأخرى​

الانتشارالوصف
REQUIREDيعيد الاستخدام أو ينشئ (افتراضي)
REQUIRES_NEWدائمًا معاملة جديدة
SUPPORTSمعاملة إذا كانت موجودة بالفعل، وإلا غير معاملاتي
MANDATORYمعاملة إلزامية (استثناء إذا لم يكن كذلك)
NOT_SUPPORTEDتعليق المعاملة الحالية
NEVERاستثناء إذا كانت المعاملة موجودة
NESTEDمعاملة متداخلة (نقاط حفظ)

5. مستويات العزل​

يحدد العزل كيف ترى المعاملات تعديلات المعاملات الأخرى.

مشاكل العزل​

1. قراءة قذرة (Dirty Read)​

المعاملة A: UPDATE الرصيد = 400 (لم يتم COMMIT بعد)
المعاملة B: SELECT الرصيد → 400 (قراءة قيمة غير ملتزمة!)
المعاملة A: ROLLBACK
النتيجة: B قرأت قيمة لم تعد موجودة

2. قراءة غير قابلة للتكرار (Non-Repeatable Read)​

المعاملة A: SELECT الرصيد → 500
المعاملة B: UPDATE الرصيد = 400، COMMIT
المعاملة A: SELECT الرصيد → 400 (قيمة مختلفة!)

3. قراءة شبحية (Phantom Read)​

المعاملة A: SELECT COUNT(*) WHERE status='ACTIVE' → 10
المعاملة B: INSERT INTO users (status='ACTIVE')، COMMIT
المعاملة A: SELECT COUNT(*) WHERE status='ACTIVE' → 11 (تسجيل جديد!)

مستويات العزل​

المستوىقراءة قذرةغير قابلة للتكرارشبحية
READ_UNCOMMITTED❌ ممكن❌ ممكن❌ ممكن
READ_COMMITTED✅ ممنوع❌ ممكن❌ ممكن
REPEATABLE_READ✅ ممنوع✅ ممنوع❌ ممكن
SERIALIZABLE✅ ممنوع✅ ممنوع✅ ممنوع

الاستخدام​

// افتراضيًا: READ_COMMITTED (حسب قاعدة البيانات)
@Transactional
public void normalOperation() { }

// للتقارير المالية: SERIALIZABLE
@Transactional(isolation = Isolation.SERIALIZABLE)
public BigDecimal calculateTotalBalance() { }

// للقراءات البسيطة: READ_COMMITTED يكفي
@Transactional(isolation = Isolation.READ_COMMITTED)
public User getUser(Long id) { }
حذر

SERIALIZABLE هو الأكثر أمانًا ولكنه الأكثر بطئًا. استخدمه فقط عند الضرورة!


6. التراجع والاستثناءات​

السلوك الافتراضي​

@Transactional
public void operation() {
repo.save(entity);

throw new RuntimeException(); // ✅ تراجع تلقائي
}

@Transactional
public void operation() throws IOException {
repo.save(entity);

throw new IOException(); // ❌ لا يوجد تراجع! (استثناء مفحوص)
}

القاعدة الافتراضية:

  • ✅ RuntimeException (غير مفحوص) → تراجع
  • ❌ Exception (مفحوص) → لا يوجد تراجع

فرض التراجع​

// تراجع على جميع الاستثناءات
@Transactional(rollbackFor = Exception.class)
public void operation() throws Exception {
repo.save(entity);
throw new IOException(); // ✅ تراجع الآن
}

// تراجع على استثناءات محددة
@Transactional(rollbackFor = {IOException.class, CustomException.class})
public void operation() throws Exception { }

عدم التراجع​

// عدم التراجع على استثناءات معينة
@Transactional(noRollbackFor = BusinessException.class)
public void operation() {
repo.save(entity);
throw new BusinessException("انتهاك قاعدة العمل");
// لا يوجد تراجع، الكيان محفوظ
}

7. معاملات للقراءة فقط​

تحسين للقراءات​

@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepo.findAll();
}

@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepo.findById(id).orElseThrow();
}

الفوائد:

  • Hibernate يعطل فحص التغييرات (أسرع)
  • بعض قواعد البيانات تحسن الاستعلامات للقراءة فقط
  • توثيق واضح للنية

على مستوى الفئة​

@Service
@Transactional(readOnly = true) // افتراضيًا: للقراءة فقط
public class UserService {

public List<User> getAllUsers() { } // readOnly = true
public User getUser(Long id) { } // readOnly = true

@Transactional // تجاوز: قراءة وكتابة
public void createUser(User user) { }
}

8. المهلة الزمنية​

تحديد مدة المعاملة​

@Transactional(timeout = 5)  // 5 ثوانٍ كحد أقصى
public void longOperation() {
// إذا استغرق أكثر من 5 ثوانٍ → TransactionTimedOutException
}

حالات الاستخدام:

  • استعلامات معقدة قد تتسبب في حظر
  • الحماية من حالات الجمود
  • ضمان استجابة النظام

9. الفخاخ الشائعة​

❌ الفخ 1: الاستدعاء الذاتي (الأكثر شيوعًا!)​

@Service
public class UserService {

public void methodA() {
this.methodB(); // ❌ @Transactional تم تجاهله!
}

@Transactional
public void methodB() {
// لا توجد معاملة لأنه تم استدعاؤه من نفس الفئة
}
}

لماذا؟ الوكيل لا يمكنه اعتراض إلا الاستدعاءات الخارجية.

الحل:

@Service
public class UserService {

@Autowired
private UserService self; // حقن الذات

public void methodA() {
self.methodB(); // ✅ يمر عبر الوكيل
}

@Transactional
public void methodB() {
// معاملة نشطة!
}
}

❌ الفخ 2: استثناء مبلوع​

@Transactional
public void operation() {
try {
riskyOperation();
} catch (Exception e) {
log.error("خطأ تم تجاهله", e); // ❌ استثناء مبلوع
// المعاملة لا تعرف أن هناك خطأ!
}
}

الحل:

@Transactional
public void operation() {
try {
riskyOperation();
} catch (Exception e) {
log.error("خطأ", e);
throw e; // ✅ إعادة الرمي لتفعيل التراجع
}
}

❌ الفخ 3: @Transactional على الواجهة​

// ❌ لا يعمل مع الوكلاء CGLIB (افتراضيًا)
public interface UserService {
@Transactional
void createUser(User user);
}

// ✅ ضع التعليق التوضيحي على التنفيذ
@Service
public class UserServiceImpl implements UserService {
@Transactional
public void createUser(User user) { }
}

❌ الفخ 4: طريقة نهائية​

@Service
public class UserService {

@Transactional
public final void operation() { // ❌ CGLIB لا يمكنه إنشاء الوكيل
// تم تجاهل المعاملة!
}
}

الحل: قم بإزالة الكلمة الرئيسية final.


10. ملخص​

ملخص السمات​

@Transactional(
propagation = Propagation.REQUIRED, // سلوك المعاملة
isolation = Isolation.READ_COMMITTED, // مستوى العزل
timeout = 30, // المهلة الزمنية بالثواني
readOnly = false, // للقراءة فقط؟
rollbackFor = Exception.class, // التراجع على أي استثناءات؟
noRollbackFor = BusinessException.class // لا يوجد تراجع على أي استثناءات؟
)
public void operation() { }

التكوينات الشائعة​

// إنشاء/تعديل قياسي
@Transactional
public void createUser(User user) { }

// قراءة محسنة
@Transactional(readOnly = true)
public User getUser(Long id) { }

// عملية حرجة (تدقيق)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditLog(String action) { }

// معالجة الدفع
@Transactional(
isolation = Isolation.SERIALIZABLE,
rollbackFor = Exception.class
)
public void processPayment(Payment payment) { }

قائمة التحقق​

✅ ضع @Transactional على طرق الخدمة (ليس على المتحكم)
✅ استخدم readOnly = true للقراءات
✅ فهم الانتشار المستخدم
✅ تحقق من أن الاستثناءات تثير التراجع
✅ تجنب الاستدعاء الذاتي
✅ لا تبلع الاستثناءات

🎯 الخطوة التالية​

الآن بعد أن أتقنت المعاملات، دعنا نتعلم كيفية إصدار مخطط قاعدة البيانات الخاص بك باستخدام Flyway.

➡️ الفصل 3: ترحيل المخطط باستخدام Flyway


⬅️ العودة إلى الوحدة 7