عندما لا يكفي CRUD العادي: التعرف على CQRS و Event Sourcing
\nتخيل أنك تبني نظامًا لحجز التذاكر. كل شيء بسيط طالما أن المستخدمين عشرة. ولكن عندما يصبحون ألفًا، ثم مليونًا، تبدأ المشاكل: يتم حظر قواعد البيانات، وتتعارض الاستعلامات، ويصبح من المستحيل استعادة تاريخ التغييرات. هل هذا الموقف مألوف؟
\nفي عالم تطوير Go، حيث الأداء والتوازي هما كل شيء، هناك نمطان معماريان يساعدان في حل هذه المشاكل: CQRS (فصل مسؤولية الأوامر والاستعلامات) و Event Sourcing. اليوم سنقوم بتحليلهما عمليًا - مع الكود والأمثلة وسيناريوهات الاستخدام الواقعية.
\n\nما هو CQRS بكلمات بسيطة؟
\nCQRS هو فصل المسؤولية بين الأوامر (الكتابة) والاستعلامات (القراءة). في CRUD التقليدي، نموذج بيانات واحد مسؤول عن كل من الإنشاء والقراءة. في CQRS، نقوم بإنشاء نموذجين منفصلين:
\n- \n
- نموذج الأوامر (Command model) - مسؤول عن تغيير الحالة (إنشاء، تحديث، حذف) \n
- نموذج الاستعلامات (Query model) - مسؤول فقط عن قراءة البيانات \n
هذا يسمح بتحسين كل جانب بشكل مستقل: يمكن أن تكون الكتابة صارمة في التحقق من الصحة، بينما تكون القراءة غير معيارية وسريعة. في Go، هذا فعال بشكل خاص بفضل goroutines و channels.
\n\nمثال بسيط على CQRS في Go
\nلنقم بإنشاء نظام أساسي لإدارة الطلبات. لنبدأ بالأوامر:
\n\ntype CreateOrderCommand struct {\n OrderID string\n UserID string\n ProductID string\n Amount float64\n}\n\ntype CommandHandler struct {\n repo *OrderRepository\n}\n\nfunc (h *CommandHandler) HandleCreateOrder(ctx context.Context, cmd CreateOrderCommand) error {\n order := Order{\n ID: cmd.OrderID,\n UserID: cmd.UserID,\n ProductID: cmd.ProductID,\n Amount: cmd.Amount,\n Status: "pending",\n CreatedAt: time.Now(),\n }\n return h.repo.Save(ctx, order)\n}\n\nالآن لنقم بإنشاء نموذج منفصل للقراءة:
\n\ntype OrderQuery struct {\n repo *ReadRepository\n}\n\ntype OrderView struct {\n OrderID string\n UserName string\n ProductName string\n Total float64\n Status string\n}\n\nfunc (q *OrderQuery) GetOrderSummary(ctx context.Context, orderID string) (*OrderView, error) {\n return q.repo.GetOrderView(ctx, orderID)\n}\n\nلاحظ: في OrderView توجد بالفعل بيانات غير معيارية (اسم المستخدم، اسم المنتج) يتم جمعها من مصادر مختلفة. هذا يسرع القراءة بعشرات المرات.
Event Sourcing: لا نسجل الحالة، بل نسجل الأحداث
\nيغير Event Sourcing نهج تخزين البيانات: بدلاً من تخزين الحالة الحالية للكائن، نقوم بتخزين سلسلة من الأحداث التي أدت إلى هذه الحالة. وهذا يعطي قوة خارقة: تاريخ كامل للتغييرات وإمكانية "إرجاع" الحالة إلى الوراء.
\n\nتنفيذ Event Sourcing في Go
\nلنبدأ بأنواع الأحداث الأساسية:
\n\ntype Event struct {\n ID string\n AggregateID string\n Type string\n Data []byte\n Timestamp time.Time\n Version int\n}\n\ntype EventStore interface {\n Save(ctx context.Context, events []Event) error\n Load(ctx context.Context, aggregateID string) ([]Event, error)\n}\n\nالآن لنقم بتنفيذ المجمع (Aggregate) - الكائن الذي يستعيد حالته من الأحداث:
\n\ntype OrderAggregate struct {\n ID string\n Status string\n Items []OrderItem\n Version int\n}\n\nfunc (a *OrderAggregate) Apply(event Event) {\n switch event.Type {\n case "OrderCreated":\n var data OrderCreatedData\n json.Unmarshal(event.Data, &data)\n a.ID = data.OrderID\n a.Status = "created"\n case "OrderConfirmed":\n a.Status = "confirmed"\n case "OrderShipped":\n a.Stat