طبقة الخدمة وإدارة المعاملات
المعاملات هي حراس تناسق البيانات—تقديم طلب وخصم المخزون يجب أن يُنجزا بشكل ذري؛ إما أن تنجح المعاملة بالكامل أو تتراجع بالكامل.
1. ما ستتعلمه
- أنماط تصميم طبقة الخدمة: واجهة + تنفيذ، تعليق
@Serviceوالحقن التابع - سلوك النشر
@Transactional(REQUIRED، REQUIRES_NEW، NESTED) - قواعد تراجع المعاملات: تهيئة
rollbackFor/noRollbackFor - معاملات القراءة فقط
@Transactional(readOnly = true)لتحسين الأداء - Alice: تنفيذ معاملة ذرية لخصم المخزون عند تقديم الطلب
2. قصة حقيقية من مطوّر أعمال
(1) نقطة الألم: عدم تناسق خصومات المخزون عند تقديم الطلبات
اكتشفت Alice خطأً خطيراً في OrderFlow: بعد أن يُقدم المستخدم طلباً بنجاح، لم يتم خصم مخزون المنتج. كان ذلك لأن إنشاء الطلب وخصم المخزون عمليتان منفصلتان؛ عندما يُنشأ الطلب بنجاح لكن خصم المخزون يفشل، يحدث عدم تناسق في البيانات. أبلغ Charlie أن هناك بالفعل 5000 طلب بيع زائد، مما أدى إلى خسارة تجاوزت 10000 دولار للشركة.
(2) حلول المعاملات الإعلانية
تعليق @Transactional في Spring يجعل إدارة المعاملات سطراً واحداً:
@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) البنية الطبقية
graph TD
A["المتحكم<br/>طلب/استجابة"] --> B["الخدمة<br/>منطق الأعمال<br/>@Transactional"]
B --> C["المستودع<br/>الوصول للبيانات"]
C --> D["قاعدة البيانات"]
| الطبقة | المسؤوليات | ملاحظات | اصطلاحات التسمية |
|---|---|---|---|
| المتحكم | استقبال الطلبات، التحقق من المعاملات، إرجاع الاستجابات | @RestController |
*Controller |
| الخدمة | منطق الأعمال، تنسيق المعاملات | @Service |
*Service / *ServiceImpl |
| المستودع | الوصول للبيانات | @Repository |
*Repository |
(1) ▶ مثال: واجهة الخدمة والتنفيذ
// 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"));
}
}
الناتج:
// التنفيذ ناجح
4. سلوك نشر @Transactional
(1) سبعة أنواع من سلوكيات النشر
| سلوك النشر | المعنى | السيناريوهات النموذجية |
|---|---|---|
REQUIRED (الافتراضي) |
الانضمام للمعاملة، عدم إنشاء واحدة | الغالبية العظمى من طرق الأعمال |
REQUIRES_NEW |
دائماً إنشاء معاملة جديدة وتعليق المعاملة الحالية | سجلات التدقيق (لا تتأثر بتراجع الطبقة الخارجية) |
NESTED |
معاملة متداخلة (نقطة حفظ): إذا تراجعت المعاملة الخارجية، تتراجع الداخلية أيضاً | عمليات فرعية يمكن تراجعها بشكل مستقل |
SUPPORTS |
المشاركة في المعاملة؛ تنفيذ غير معاملاتي | طرق الاستعلام |
NOT_SUPPORTED |
تنفيذ غير معاملاتي؛ تعليق المعاملة الحالية | عمليات لا تتطلب معاملة |
MANDATORY |
يجب الاستدعاء ضمن معاملة؛ وإلا يُطلق استثناء | طرق تتطلب معاملة |
NEVER |
يجب أن يكون الاستدعاء غير معاملاتي؛ وإلا يُطلق استثناء | العمليات المعاملاتية غير مسموح بها |
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 يضمن سجلات التدقيق
@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);
}
}
الناتج:
// التنفيذ ناجح
REQUIRES_NEW يؤكد سجلات التدقيق في معاملة منفصلة حتى لا تُفقد السجلات إذا تراجعت المعاملة الخارجية.
5. قواعد تراجع المعاملات
(1) سلوك التراجع الافتراضي
بشكل افتراضي، معاملات Spring تتراجع فقط عند RuntimeException وError؛ لا تتراجع بسبب الاستثناءات المُتحققة (checked exceptions).
(1) ▶ مثال: تهيئة rollbackFor
@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);
}
}
الناتج:
// التنفيذ ناجح
| التهيئة | المعنى | السيناريوهات المناسبة |
|---|---|---|
rollbackFor = Exception.class |
تراجع عند جميع الاستثناءات | تهيئة افتراضية موصى بها |
rollbackFor = BusinessException.class |
تراجع عند استثناء مخصص فقط | تحكم دقيق |
noRollbackFor = ValidationException.class |
عدم التراجع عند استثناءات محددة | استثناءات معينة لا تُ触发 تراجعاً |
6. تحسين معاملات القراءة فقط
(1) ▶ مثال: معاملات القراءة فقط
@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);
}
}
الناتج:
// التنفيذ ناجح
| البُعد | معاملات القراءة-الكتابة | معاملات القراءة فقط |
|---|---|---|
| التحقق من الأوساخ | مُفعّل (مقارنة حقل بحقل) | متجاوز |
| الأداء | أبطأ | أسرع بنسبة 20-30% |
| عملية الكتابة | مسموحة | ممنوعة (الكتابة تطلق استثناء) |
| مناسب | عمليات CUD | عمليات الاستعلام |
@Transactional(readOnly = true)؛ هذا تحسين أداء بدون تكلفة.
7. المعاملة الذرية لتقديم الطلبات وخصم المخزون
(1) ▶ مثال: عملية تقديم الطلبات الكاملة
@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;
}
}
الناتج:
// التنفيذ ناجح
8. مثال شامل: التنفيذ الكامل لطبقة خدمة OrderFlow
// 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"));
}
}
❓ أسئلة شائعة
@Lazy private OrderService self;.readOnly=true؟spring.jpa.show-sql=true لمراقبة تنفيذ SQL؛ 2) اضبط logging.level.org.springframework.transaction=DEBUG لعرض سجل المعاملات؛ 3) ارمِ استثناءً عمداً للتحقق من التراجع.📖 ملخص
- طبقة الخدمة تغلف منطق الأعمال؛ المتحكم يستدعي الخدمة، والخدمة تستدعي المستودع
@Transactionalنشر REQUIRED افتراضي؛ RuntimeException يُتراجع تلقائياًREQUIRES_NEWيُستخدم للمعاملات المستقلة (مثل سجلات التدقيق)، وNESTEDللمعاملات المتداخلةrollbackFor = Exception.classيضمن التراجع عند جميع الاستثناءات@Transactional(readOnly = true)تحسين بدون تكلفة لطرق الاستعلام- المعاملات ذات الاستدعاءات المتبادلة بين طرق نفس النوع لا تسري؛ وهذا قيد في آلية بروكسي Spring
📝 تمارين
-
تمرين أساسي (الصعوبة ⭐): أضف تعليق
@Transactionalإلى ProductService وOrderService في OrderFlow للتحقق من ذرية خصم المخزون عند تقديم الطلب—ارمِ استثناءً عمداً بعد خصم المخزون للتأكد من أن الطلب لن يُنشأ. -
تمرين متقدم (الصعوبة: ⭐⭐): أنشئ AuditService يستخدم سلوك النشر
REQUIRES_NEWلضمان عدم فقدان سجلات التدقيق حتى لو تراجعت معاملة إنشاء الطلب. -
سؤال تحدٍ (الصعوبة: ⭐⭐⭐): حاكِ سيناريو طلب متزامن (طلبان يحاولان شراء عنصر واحد متبقي في المخزون)، لاحظ مشكلة البيع الزائد، وفكر في كيفية حلها باستخدام القفل المتفائل (
@Version) أو القفل المتشائم (SELECT FOR UPDATE).



