الفصل 2: إدارة المعاملات (@Transactional)
مقدمة
تخيل تحويلًا مصرفيًا: تقوم بخصم 100€ من الحساب A وتضيف 100€ إلى الحساب B. ماذا يحدث إذا فشل الإيداع بعد الخصم؟ بدون معاملة، ستختفي 100€!
تضمن المعاملات أن تكون عمليات البيانات الخاصة بك موثوقة ومتسقة.
🎯 أهداف الفصل
في نهاية هذا الفصل، ستعرف:
- ✅ فهم خصائص ACID
- ✅ استخدام @Transactional بشكل صحيح
- ✅ اختيار مستوى الانتشار المناسب
- ✅ فهم مستويات العزل
- ✅ إدارة التراجع بشكل صحيح
- ✅ تجنب الفخاخ الشائعة
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