404 Not Found

404 Not Found


nginx

طبقة الخدمة وإدارة المعاملات

المعاملات هي حراس تناسق البيانات—تقديم طلب وخصم المخزون يجب أن يُنجزا بشكل ذري؛ إما أن تنجح المعاملة بالكامل أو تتراجع بالكامل.

1. ما ستتعلمه


2. قصة حقيقية من مطوّر أعمال

(1) نقطة الألم: عدم تناسق خصومات المخزون عند تقديم الطلبات

اكتشفت Alice خطأً خطيراً في OrderFlow: بعد أن يُقدم المستخدم طلباً بنجاح، لم يتم خصم مخزون المنتج. كان ذلك لأن إنشاء الطلب وخصم المخزون عمليتان منفصلتان؛ عندما يُنشأ الطلب بنجاح لكن خصم المخزون يفشل، يحدث عدم تناسق في البيانات. أبلغ Charlie أن هناك بالفعل 5000 طلب بيع زائد، مما أدى إلى خسارة تجاوزت 10000 دولار للشركة.

(2) حلول المعاملات الإعلانية

تعليق @Transactional في Spring يجعل إدارة المعاملات سطراً واحداً:

JAVA
@Transactional
public Order createOrder(CreateOrderRequest request) {
    Product product = productRepository.findById(request.productId()).orElseThrow();
    product.deductStock(request.quantity());    // خصم المخزون
    Order order = new Order(product, request.quantity());
    return orderRepository.save(order);         // إنشاء الطلب
}

فشل خصم المخزون؟ تتراجع المعاملة بالكامل تلقائياً، ولا يُنشأ الطلب.

(3) العوائد

بعد أن أضافت Alice @Transactional، أصبح خصم المخزون عند تقديم الطلبات يضمن الذرية، مما قضى تماماً على مشكلة البيع الزائد. انخفض عدد شكاوى Charlie إلى الصفر.


3. أنماط التصميم في طبقة الخدمة

(1) البنية الطبقية

100%
graph TD
    A["المتحكم<br/>طلب/استجابة"] --> B["الخدمة<br/>منطق الأعمال<br/>@Transactional"]
    B --> C["المستودع<br/>الوصول للبيانات"]
    C --> D["قاعدة البيانات"]
الطبقة المسؤوليات ملاحظات اصطلاحات التسمية
المتحكم استقبال الطلبات، التحقق من المعاملات، إرجاع الاستجابات @RestController *Controller
الخدمة منطق الأعمال، تنسيق المعاملات @Service *Service / *ServiceImpl
المستودع الوصول للبيانات @Repository *Repository

(1) ▶ مثال: واجهة الخدمة والتنفيذ

JAVA
// OrderService.java (واجهة)
package com.orderflow.service;

import com.orderflow.model.Order;
public interface OrderService {
    Order createOrder(Long productId, Integer quantity);
    Order cancelOrder(Long orderId);
    Order getOrder(Long orderId);
}

// OrderServiceImpl.java (تنفيذ)
package com.orderflow.service;

import com.orderflow.model.*;
import com.orderflow.repository.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    public OrderServiceImpl(OrderRepository orderRepository,
                            ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }

    @Override
    @Transactional
    public Order createOrder(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        product.deductStock(quantity);
        Order order = new Order();
        order.addItem(product, quantity);
        return orderRepository.save(order);
    }

    @Override
    @Transactional(readOnly = true)
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
    }
}

الناتج:

TEXT
// التنفيذ ناجح

4. سلوك نشر @Transactional

(1) سبعة أنواع من سلوكيات النشر

سلوك النشر المعنى السيناريوهات النموذجية
REQUIRED (الافتراضي) الانضمام للمعاملة، عدم إنشاء واحدة الغالبية العظمى من طرق الأعمال
REQUIRES_NEW دائماً إنشاء معاملة جديدة وتعليق المعاملة الحالية سجلات التدقيق (لا تتأثر بتراجع الطبقة الخارجية)
NESTED معاملة متداخلة (نقطة حفظ): إذا تراجعت المعاملة الخارجية، تتراجع الداخلية أيضاً عمليات فرعية يمكن تراجعها بشكل مستقل
SUPPORTS المشاركة في المعاملة؛ تنفيذ غير معاملاتي طرق الاستعلام
NOT_SUPPORTED تنفيذ غير معاملاتي؛ تعليق المعاملة الحالية عمليات لا تتطلب معاملة
MANDATORY يجب الاستدعاء ضمن معاملة؛ وإلا يُطلق استثناء طرق تتطلب معاملة
NEVER يجب أن يكون الاستدعاء غير معاملاتي؛ وإلا يُطلق استثناء العمليات المعاملاتية غير مسموح بها
100%
sequenceDiagram
    participant C as Controller
    participant S1 as createOrder (REQUIRED)
    participant S2 as recordAudit (REQUIRES_NEW)
    participant S3 as deductStock (REQUIRED)

    C->>S1: بدء Tx1
    S1->>S3: الانضمام إلى Tx1
    S3-->>S1: OK
    S1->>S2: تعليق Tx1، بدء Tx2
    S2-->>S1: تأكيد Tx2، استئناف Tx1
    alt تراجع Tx1
        S1-->>C: تراجع Tx1 (Tx2 غير متأثر)
    end

(1) ▶ مثال: REQUIRES_NEW يضمن سجلات التدقيق

JAVA
@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    public AuditService(AuditLogRepository auditLogRepository) {
        this.auditLogRepository = auditLogRepository;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAudit(String action, String detail) {
        AuditLog log = new AuditLog(action, detail, Instant.now());
        auditLogRepository.save(log);
    }
}

الناتج:

TEXT
// التنفيذ ناجح
📌 نقطة رئيسية: REQUIRES_NEW يؤكد سجلات التدقيق في معاملة منفصلة حتى لا تُفقد السجلات إذا تراجعت المعاملة الخارجية.


5. قواعد تراجع المعاملات

(1) سلوك التراجع الافتراضي

بشكل افتراضي، معاملات Spring تتراجع فقط عند RuntimeException وError؛ لا تتراجع بسبب الاستثناءات المُتحققة (checked exceptions).

(1) ▶ مثال: تهيئة rollbackFor

JAVA
@Service
public class OrderServiceImpl implements OrderService {

    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long productId, Integer quantity) throws Exception {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        product.deductStock(quantity);
        Order order = new Order();
        order.addItem(product, quantity);
        return orderRepository.save(order);
    }
}

الناتج:

TEXT
// التنفيذ ناجح
التهيئة المعنى السيناريوهات المناسبة
rollbackFor = Exception.class تراجع عند جميع الاستثناءات تهيئة افتراضية موصى بها
rollbackFor = BusinessException.class تراجع عند استثناء مخصص فقط تحكم دقيق
noRollbackFor = ValidationException.class عدم التراجع عند استثناءات محددة استثناءات معينة لا تُ触发 تراجعاً

6. تحسين معاملات القراءة فقط

(1) ▶ مثال: معاملات القراءة فقط

JAVA
@Service
public class OrderQueryService {

    private final OrderRepository orderRepository;

    public OrderQueryService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional(readOnly = true)
    public Order getOrder(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }

    @Transactional(readOnly = true)
    public Page<Order> listOrders(String status, Pageable pageable) {
        return orderRepository.findByStatus(status, pageable);
    }
}

الناتج:

TEXT
// التنفيذ ناجح
البُعد معاملات القراءة-الكتابة معاملات القراءة فقط
التحقق من الأوساخ مُفعّل (مقارنة حقل بحقل) متجاوز
الأداء أبطأ أسرع بنسبة 20-30%
عملية الكتابة مسموحة ممنوعة (الكتابة تطلق استثناء)
مناسب عمليات CUD عمليات الاستعلام
💡 نصيحة: يجب أن تتضمن جميع طرق الاستعلام @Transactional(readOnly = true)؛ هذا تحسين أداء بدون تكلفة.


7. المعاملة الذرية لتقديم الطلبات وخصم المخزون

(1) ▶ مثال: عملية تقديم الطلبات الكاملة

JAVA
@Service
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final AuditService auditService;

    public OrderServiceImpl(OrderRepository orderRepository,
                            ProductRepository productRepository,
                            AuditService auditService) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
        this.auditService = auditService;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long productId, Integer quantity) {
        // 1. البحث عن المنتج
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found: " + productId));

        // 2. التحقق من المخزون
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock: available="
                + product.getStock() + ", requested=" + quantity);
        }

        // 3. خصم المخزون (في نفس المعاملة)
        product.deductStock(quantity);

        // 4. إنشاء طلب مع عنصر الطلب
        Order order = new Order();
        order.addItem(product, quantity);
        Order saved = orderRepository.save(order);

        // 5. سجل التدقيق (في معاملة منفصلة عبر REQUIRES_NEW)
        auditService.logAudit("CREATE_ORDER",
            "Order " + saved.getId() + " created for product " + productId);

        return saved;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Order cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found: " + orderId));

        if (!"PENDING".equals(order.getStatus())) {
            throw new RuntimeException("Cannot cancel order with status: " + order.getStatus());
        }

        // استعادة المخزون لكل عنصر
        for (OrderItem item : order.getItems()) {
            item.getProduct().addStock(item.getQuantity());
        }

        order.setStatus("CANCELLED");
        auditService.logAudit("CANCEL_ORDER", "Order " + orderId + " cancelled");
        return order;
    }
}

الناتج:

TEXT
// التنفيذ ناجح

8. مثال شامل: التنفيذ الكامل لطبقة خدمة OrderFlow

JAVA
// ProductService.java
package com.orderflow.service;

import com.orderflow.model.Product;
import com.orderflow.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional(readOnly = true)
    public Product getProduct(Long id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("Product not found"));
    }

    @Transactional(readOnly = true)
    public Page<Product> listProducts(Pageable pageable) {
        return productRepository.findAll(pageable);
    }

    @Transactional(readOnly = true)
    public List<Product> searchProducts(String keyword) {
        return productRepository.findByNameContaining(keyword);
    }

    @Transactional
    public Product createProduct(String name, BigDecimal price, Integer stock) {
        return productRepository.save(new Product(name, price, stock));
    }

    @Transactional
    public Product updateProduct(Long id, String name, BigDecimal price, Integer stock) {
        Product product = getProduct(id);
        product.setName(name);
        product.setPrice(price);
        product.setStock(stock);
        return product;
    }
}

// OrderService.java
package com.orderflow.service;

import com.orderflow.model.Order;
import com.orderflow.model.Product;
import com.orderflow.repository.OrderRepository;
import com.orderflow.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    public OrderService(OrderRepository orderRepository,
                        ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }

    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock");
        }
        product.deductStock(quantity);
        Order order = new Order();
        order.addItem(product, quantity);
        return orderRepository.save(order);
    }

    @Transactional(rollbackFor = Exception.class)
    public Order cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
        order.getItems().forEach(item -> item.getProduct().addStock(item.getQuantity()));
        order.setStatus("CANCELLED");
        return order;
    }

    @Transactional(readOnly = true)
    public Order getOrder(Long orderId) {
        return orderRepository.findByIdWithItems(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
    }
}

❓ أسئلة شائعة

س هل يجب تطبيق تعليق @Transactional على الواجهة أم صنف التنفيذ؟
ج يُوصى بتطبيقه على طرق صنف التنفيذ. عند التطبيق على واجهة، لا تستطيع البروكسيات المبنية على CGLIB اعتراض التعليقات على الواجهات. Spring أيضاً يوصي رسمياً بتطبيقه على الأصناف الملموسة.
س هل يسري تعليق @Transactional عند استدعاء الطرق لبعضها البعض ضمن نفس الصنف؟
ج لا. معاملات Spring مبنية على البروكسي؛ عندما تستدعي طرق نفس الصنف بعضها البعض، لا يتم استدعاء البروكسي، لذا المعاملة لا تسري. الحلول: 1) تقسيمها إلى خدمات مختلفة؛ 2) حقن البروكسي الخاص بك @Lazy private OrderService self;.
س ماذا يحدث عند تنفيذ عملية كتابة عندما readOnly=true؟
ج يعتمد على قاعدة البيانات وتهيئة تجمع الاتصالات. Hibernate سيرمي استثناءً، بينما بعض مشغلات قواعد البيانات ستتجاهله. لا تنفذ عمليات كتابة ضمن معاملات القراءة فقط.
س ما الفرق بين NESTED وREQUIRES_NEW؟
ج NESTED هي معاملة متداخلة (نقطة حفظ)؛ عند تراجع المعاملة الخارجية، تتراجع الداخلية أيضاً. REQUIRES_NEW هي معاملة مستقلة؛ تراجع المعاملة الخارجية لا يؤثر على الداخلية. استخدم REQUIRES_NEW للتأكيد المستقل، واستخدم NESTED للتراجع الجزئي.
س ما هي الأسباب الشائعة لفشل المعاملات؟
ج 1) الاستدعاءات المتبادلة بين طرق نفس النوع؛ 2) الطرق ليست عامة (public)؛ 3) الاستثناءات مبتلعة بواسطة كتلة catch؛ 4) بشكل افتراضي، يتم التراجع فقط عند RuntimeException؛ الاستثناءات المُتحققة لا تسبب تراجعاً.
س كيف أتحقق مما إذا كانت المعاملة سارية؟
ج 1) فعّل spring.jpa.show-sql=true لمراقبة تنفيذ SQL؛ 2) اضبط logging.level.org.springframework.transaction=DEBUG لعرض سجل المعاملات؛ 3) ارمِ استثناءً عمداً للتحقق من التراجع.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة ⭐): أضف تعليق @Transactional إلى ProductService وOrderService في OrderFlow للتحقق من ذرية خصم المخزون عند تقديم الطلب—ارمِ استثناءً عمداً بعد خصم المخزون للتأكد من أن الطلب لن يُنشأ.

  2. تمرين متقدم (الصعوبة: ⭐⭐): أنشئ AuditService يستخدم سلوك النشر REQUIRES_NEW لضمان عدم فقدان سجلات التدقيق حتى لو تراجعت معاملة إنشاء الطلب.

  3. سؤال تحدٍ (الصعوبة: ⭐⭐⭐): حاكِ سيناريو طلب متزامن (طلبان يحاولان شراء عنصر واحد متبقي في المخزون)، لاحظ مشكلة البيع الزائد، وفكر في كيفية حلها باستخدام القفل المتفائل (@Version) أو القفل المتشائم (SELECT FOR UPDATE).

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%