تخطَّ إلى المحتوى

🏗️ شرح المايكروسيرفيس (Microservices)

نمط CQRS

الدرس 10 من 29· ⏱ 4 دقائق قراءة· 🗓 آخر تحديث: ١٦ أكتوبر ٢٠٢٦

ما هو CQRS؟

CQRS (Command Query Responsibility Segregation) يفصل بين أوامر الكتابة (Commands) واستعلامات القراءة (Queries).

لماذا؟ لأن احتياجاتهما مختلفة غالبًا. الكتابة تحتاج تحقّقًا صارمًا من قواعد العمل واتساقًا (هل الرصيد كافٍ؟ هل المنتج متوفّر؟). القراءة تحتاج شكلًا جاهزًا للعرض بسرعة، مثل ملخّص طلب فيه اسم العميل وأسماء المنتجات، بدون Joins بين خدمات متعدّدة. CQRS يعطي كل جانب نموذج بيانات خاصًا به:

Command → Write Model → Event → Read Model (مُجمَّع مسبقًا) ← Query

الفكرة الأساسية

بدلاً من API واحد للقراءة والكتابة:

// قبل CQRS — API واحد
app.post('/users', createUser);
app.get('/users/:id', getUser);
app.patch('/users/:id', updateUser);
// بعد CQRS
const writeApi = express();
writeApi.post('/users', createUser);    // Command

const readApi = express();
readApi.get('/users/:id', getUser);    // Query

مثال متكامل

import { OrderRepositoryProtocol, InsufficientFundsError } from './lib/base_repository';

// Command (طلب تنفيذ إجراء)
class CreateOrderCommand {
  constructor(
    readonly userId: string,
    readonly items: OrderItem[],
    readonly idempotencyKey: string,
  ) {}
}

// Command Handler — يستخدم Repository، لا يستدعي db.orders.save() مباشرة
class OrderCommandHandler {
  constructor(
    private readonly orderRepo: OrderRepositoryProtocol,
    private readonly paymentRepo: PaymentRepositoryProtocol,
  ) {}

  async handle(command: CreateOrderCommand): Promise<OrderEntity> {
    const existing = await this.orderRepo.findById(command.idempotencyKey);
    if (existing) return existing; // Idempotency

    const total = command.items.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
    const balance = await this.paymentRepo.getBalance(command.userId);

    if (balance < total) {
      throw new InsufficientFundsError(balance, total);
    }

    const order: OrderEntity = {
      id: command.idempotencyKey,
      userId: command.userId,
      items: command.items,
      total,
      status: 'PENDING',
      createdAt: new Date(),
    };

    await this.orderRepo.save(order);
    await eventBus.publish('order.created', { orderId: order.id });
    return order;
  }
}
import { OrderNotFoundError, OrderRepositoryProtocol } from './lib/base_repository';

// Query Side (القراءة) — يستخدم Read Model Repository منفصل
class OrderQueryHandler {
  constructor(
    private readonly readRepo: OrderRepositoryProtocol,
  ) {}

  async getOrder(id: string): Promise<OrderReadModel> {
    const order = await this.readRepo.findById(id);
    if (!order) throw new OrderNotFoundError(id);

    return {
      id: order.id,
      userName: order.userName,
      items: order.items.map(i => ({
        name: i.productName,
        quantity: i.quantity,
        price: i.unitPrice,
      })),
      total: order.total,
      status: order.status,
    };
  }

  async getOrdersByUser(userId: string): Promise<OrderReadModel[]> {
    const orders = await this.readRepo.findByUserId(userId);
    return orders.map(o => ({
      id: o.id,
      userName: o.userName,
      items: o.items.map(i => ({
        name: i.productName,
        quantity: i.quantity,
        price: i.unitPrice,
      })),
      total: o.total,
      status: o.status,
    }));
  }
}

متى نستخدم CQRS؟

  • قراءة وكتابة بنماذج مختلفة
  • أحمال قراءة وكتابة غير متوازنة
  • الحاجة لأداء عالٍ للقراءة
  • فرق مختلفة تعمل على القراءة والكتابة

Read Model منفصل

import { OrderRepositoryProtocol } from './lib/base_repository';

// مزامنة Read Model مع Write Model عبر Events — يستخدم Repository
class ReadModelProjector {
  constructor(
    private readonly writeRepo: OrderRepositoryProtocol,
    private readonly readRepo: OrderRepositoryProtocol,
  ) {}

  async onOrderCreated(event: OrderCreatedEvent): Promise<void> {
    const order = await this.writeRepo.findById(event.orderId);
    if (!order) return;

    // Read Model منفصل — قد يكون بقاعدة بيانات مختلفة
    await this.readRepo.save({
      id: order.id,
      userId: order.userId,
      userName: order.userName,
      items: order.items,
      total: order.total,
      status: 'CREATED',
      createdAt: new Date(),
    });
  }
}

CQRS مع Event Sourcing

import { InsufficientFundsError } from './lib/base_repository';

// Aggregate Root مع Event Sourcing
class Account {
  private balance: number = 0;
  private readonly events: DomainEvent[] = [];

  apply(event: DomainEvent): void {
    if (event.type === 'DEPOSIT') this.balance += event.amount;
    if (event.type === 'WITHDRAW') {
      if (this.balance < event.amount) {
        throw new InsufficientFundsError(this.balance, event.amount);
      }
      this.balance -= event.amount;
    }
  }

  // Command — يتحقق من صحة العملية قبل تطبيقها
  deposit(amount: number): void {
    this.apply({ type: 'DEPOSIT', amount });
  }

  withdraw(amount: number): void {
    this.apply({ type: 'WITHDRAW', amount });
    // Event Store عبر Repository، ليس eventStore.save() مباشراً
  }

  // Query بسيط — يقرأ من الحالة الحالية
  getBalance(): number {
    return this.balance;
  }

  getUncommittedEvents(): readonly DomainEvent[] {
    return this.events;
  }
}

الثمن: الاتساق النهائي (Eventual Consistency)

نموذج القراءة يُحدَّث بعد وصول الحدث، لا لحظة الكتابة. يعني بعد إنشاء الطلب مباشرة قد لا يظهر في "طلباتي" لجزء من الثانية أو أكثر. صمّم الواجهة على هذا الأساس: مثلًا اعرض الطلب الجديد من رد أمر الإنشاء نفسه، بدل إعادة الاستعلام فورًا.

أخطاء شائعة

  • CQRS افتراضيًا لكل خدمة: خدمة CRUD بسيطة لا تستفيد شيئًا، وتدفع ثمن نموذجين ومزامنة بينهما.
  • افتراض أن القراءة تتحدّث فورًا بعد الكتابة: يسبّب أخطاء "اختفى ما حفظته" في الواجهة والاختبارات.

⚠️ CQRS يزيد التعقيد — طبقه فقط عندما يكون هناك فائدة حقيقية من الفصل

🎯 التالي: نمط Saga

شرح نمط CQRS — المايكروسيرفيس (Microservices) بالعربي
نمط CQRS — المايكروسيرفيس (Microservices) بالعربي · The Code Fix

هل كان هذا الدرس مفيدًا؟