CQRS يُوصى به لكل مشكلة أداء في أنظمة SaaS. في أغلب الأحيان هو الإجابة الخاطئة. متى يساعد فعلاً، وكيف ننفذه في Go، وما تعلمناه من تطبيقه في الإنتاج.
نمط CQRS (فصل مسؤولية الأوامر والاستعلامات) يُوصى به لكل مشكلة أداء في أنظمة SaaS. في أغلب الأحيان هو الإجابة الخاطئة. متى يساعد فعلاً، وكيف ننفذه في Go، وما تعلمناه من تطبيقه في أنظمة SaaS إنتاجية في لبنان ومنطقة الشرق الأوسط.
ما يعنيه CQRS في الممارسة الفعلية
CQRS يعني أن العمليات التي تغيّر الحالة (الأوامر) والعمليات التي تقرأ الحالة (الاستعلامات) لها خصائص مختلفة جوهرياً وينبغي التعامل معها بمسارات كود مختلفة، وربما مخازن بيانات مختلفة.
في نظام SaaS نموذجي بدون CQRS، نموذج واحد يخدم كلا الغرضين. هذا النموذج يجب أن يكون منظماً بما يكفي ليكون الكتابة متسقة، وغير منظم بما يكفي لتكون القراءة فعّالة. حين تتعارض متطلبات نموذج الكتابة ونموذج القراءة، تبدأ بإضافة استعلامات JOIN ثقيلة لتلبية القراءات.
CQRS يفصل هذا: جانب الأوامر يعمل مع نموذج كتابة منظم محسّن للاتساق. جانب الاستعلامات يعمل مع نموذج قراءة محسّن لكيفية عرض البيانات للمستخدمين.
متى يحل CQRS مشاكل حقيقية
لمعظم منتجات SaaS، CQRS غير ضروري. قاعدة بيانات PostgreSQL مُفهرَسة جيداً تتعامل مع الكتابة والقراءة معاً في الحجم الذي ستصله معظم المنتجات.
الحالات التي يساعد فيها CQRS حقاً:
قراءات لوحة المعلومات والتحليلات المُكلفة. نظام إدارة الطلبات يخزّن الطلبات مع البنود والخصومات والشحن. لوحة المعلومات تُظهر الإيرادات اليومية وأفضل العملاء. حساب هذا من المخطط المنظّم عند كل تحميل للصفحة يتطلب استعلامات تجميع عبر ملايين الصفوف.
متطلبات البحث التي لا تتوافق مع الاستعلامات العلائقية. كتالوج منتجات يحتاج بحثاً نصياً كاملاً وتصفية متعددة الأبعاد لا يعمل بكفاءة في PostgreSQL.
نماذج تفويض مختلفة للقراءة مقابل الكتابة. نموذج الكتابة يحتاج إلى التحقق من قواعد الأعمال. نموذج القراءة يحتاج فقط إلى إعادة البيانات للمتصلين المصرّح لهم.
أبسط تطبيق لـ CQRS لا يزال مفيداً
ابدأ بأبسط شكل: نفس قاعدة البيانات، مسارات كود منفصلة.
// معالج الأوامر: يعمل مع نموذج الكتابة المنظّم
type CommandHandler struct { db *sql.DB }
func (h *CommandHandler) PlaceOrder(ctx context.Context, cmd PlaceOrderCommand) (int64, error) {
tx, _ := h.db.BeginTx(ctx, nil)
defer tx.Rollback()
if err := h.validateOrder(ctx, tx, cmd); err != nil { return 0, err }
orderID, err := h.insertOrder(ctx, tx, cmd)
if err != nil { return 0, err }
for _, item := range cmd.LineItems {
h.insertLineItem(ctx, tx, orderID, item)
}
return orderID, tx.Commit()
}
// معالج الاستعلامات: يعمل مع طرق عرض محسّنة للقراءة
type QueryHandler struct { db *sql.DB }
func (h *QueryHandler) ListOrders(ctx context.Context, tenantID int64) ([]OrderListItem, error) {
return h.db.QueryContext(ctx, `
SELECT o.id, c.full_name, o.total, o.status, o.item_count, o.created_at
FROM order_list_view o
JOIN customers c ON c.id = o.customer_id
WHERE o.tenant_id = $1
ORDER BY o.created_at DESC`, tenantID)
}
بناء نموذج القراءة بـ PostgreSQL Materialized Views
لتجميعات لوحة المعلومات، طرق العرض المُجسَّدة في PostgreSQL هي أبسط نموذج قراءة لا يتطلب قاعدة بيانات ثانية:
CREATE MATERIALIZED VIEW order_daily_summary AS
SELECT
tenant_id,
date_trunc('day', created_at) AS day,
COUNT(*) AS order_count,
SUM(total) AS revenue,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
WHERE status NOT IN ('cancelled', 'refunded')
GROUP BY tenant_id, date_trunc('day', created_at);
-- تحديث طريقة العرض كل 5 دقائق
REFRESH MATERIALIZED VIEW CONCURRENTLY order_daily_summary;
CONCURRENTLY تسمح باستمرار القراءة أثناء التحديث. المقايضة هي بيانات قديمة قليلاً: إذا كان التحديث يعمل كل خمس دقائق، تُظهر لوحة المعلومات بيانات تأخرت حتى خمس دقائق. لمعظم لوحات الأعمال، هذا مقبول.
لخدمات Go، شغّل التحديث من مهمة خلفية مجدولة:
func (s *Scheduler) RefreshViews(ctx context.Context) error {
views := []string{"order_daily_summary", "inventory_status_summary"}
for _, view := range views {
s.db.ExecContext(ctx, "REFRESH MATERIALIZED VIEW CONCURRENTLY "+view)
}
return nil
}
تحديثات نموذج القراءة القائمة على الأحداث
للنماذج في الوقت الفعلي أو القريب من الوقت الفعلي، استبدل التحديث المجدول بتحديثات مدفوعة بالأحداث. عند وضع طلب أو تحديثه، انشر حدثاً. محدّث نموذج القراءة يستهلك الحدث ويُحدّث جدول القراءة غير المنظّم:
func (u *Updater) HandleOrderPlaced(ctx context.Context, event OrderPlacedEvent) error {
_, err := u.db.ExecContext(ctx, `
INSERT INTO order_list_cache (order_id, tenant_id, customer_name, total, status)
SELECT o.id, o.tenant_id, c.full_name, o.total, o.status
FROM orders o JOIN customers c ON c.id = o.customer_id
WHERE o.id = $1
ON CONFLICT (order_id) DO UPDATE SET
status = EXCLUDED.status,
customer_name = EXCLUDED.customer_name`,
event.OrderID)
return err
}
ما يجب تجنبه: المبالغة في هندسة CQRS
أكثر الأخطاء شيوعاً هو تطبيق Event Sourcing الكامل إلى جانب CQRS. Event Sourcing يعني أن نموذج الكتابة يخزّن تسلسلاً من الأحداث بدلاً من الحالة الحالية. إنه نمط قوي لكنه يُضيف تعقيداً جوهرياً: يجب بناء Projections والحفاظ عليها، ومعالجة إعادة تشغيل الأحداث، وسطح الكود يتضاعف تقريباً.
لمعظم منتجات SaaS، Event Sourcing الكامل غير ضروري. يمكنك الحصول على فوائد CQRS بنموذج قراءة بسيط منفصل وطرق عرض PostgreSQL المُجسَّدة، دون أي بنية تحتية لـ Event Sourcing.
خطأ شائع آخر هو تطبيق CQRS قبل وجود مشكلة أداء موثّقة. منتج بـ 50 مستأجراً وحركة مرور معتدلة لا يحتاج إلى فصل القراءة عن الكتابة.
النشر في سياق SaaS لمنطقة الشرق الأوسط
لمنتجات SaaS التي تخدم الشركات في لبنان ومنطقة الشرق الأوسط، وقت استجابة لوحة المعلومات هو مصدر قلق حقيقي لتجربة المستخدم. المشغّلون التجاريون الذين يتحققون من المبيعات اليومية وحالة المخزون والطلبات المعلّقة يتوقعون تحميل لوحة مسرعة.
مخطط منظّم يتطلب تجميع ملايين الصفوف عند كل تحميل للوحة سيتدهور في النهاية إلى أوقات تحميل 3-5 ثوان. طريقة عرض مُجسَّدة أو ذاكرة تخزين مؤقت للقراءة تُبقي لوحات المعلومات سريعة بغض النظر عن حجم البيانات.
دروس من الإنتاج
CQRS في أنظمة Go SaaS يتلخص في بعض الحقائق العملية:
- ابدأ بأبسط شكل: نفس قاعدة البيانات، معالجات أوامر واستعلامات منفصلة.
- استخدم PostgreSQL Materialized Views للتجميعات قبل اللجوء إلى قاعدة بيانات قراءة منفصلة.
- تحديثات نموذج القراءة المدفوعة بالأحداث مناسبة عندما يكون تأخر 5 دقائق كثيراً لميزة معينة.
- لا تُدخل Event Sourcing إلى جانب CQRS إلا إذا كان النطاق يتطلب إعادة تشغيل الأحداث.
- طبّق CQRS حيث يوجد عدم توافق موثّق بين متطلبات نموذج الكتابة ومتطلبات نموذج القراءة.
هل تحتاج مساعدة في تصميم بنية السيرفر الخلفي؟
Voxire تصمم أنظمة Go SaaS للشركات في لبنان ومنطقة الشرق الأوسط. إذا كان سيرفرك الخلفي يعاني من أداء لوحة المعلومات أو متطلبات قراءة معقدة، نساعدك في تصميم البنية المناسبة.
https://voxire.com/get-a-quote/



