Database Engineering

تسلط بر مدل‌سازی داده برای رویداد-محوری: پروجکشن‌ها و تکامل طرحواره

رویداد-محوری (ES) یک الگوی معماری قدرتمند است که وضعیت یک برنامه را به صورت دنباله‌ای از رویدادها ذخیره می‌کند، نه فقط وضعیت فعلی. اگرچه ES مزایای شگفت‌انگیزی مانند قابلیت حسابرسی، پرس‌وجوی زمانی و قابلیت پخش مجدد ارائه می‌دهد، اما پیچیدگی قابل توجهی هنگام مدل‌سازی داده‌ها، به ویژه در نحوه خواندن آن‌ها (پروجکشن‌ها) و نحوه مدیریت تغییرات در طول زمان (تکامل طرحواره) ایجاد می‌کند.

برای مهندسان پایگاه داده و معماران بک‌اند، پیمایش در تقاطع رویدادهای غیرقابل تغییر و مدل‌های خواندنی قابل تغییر، یکی از چالش‌برانگیزترین جنبه‌های ساخت سیستم‌های توزیع‌شده مقیاس‌پذیر است. این پست به بررسی مکانیک‌های ساخت پروجکشن‌های مقاوم و استراتژی‌های تکامل طرحواره در یک محیط رویداد-محور می‌پردازد.

سمت خواندن: طراحی پروجکشن‌های کارآمد

در یک معماری رویداد-محور، «سمت نوشتن» شامل ریشه‌های تجمیع (Aggregate Roots) و رویدادها است. «سمت خواندن» شامل پروجکشن‌ها است. یک پروجکشن فرآیندی است که رویدادها را مصرف کرده و یک مخزن بهینه‌شده برای خواندن، مانند یک پایگاه داده رابطه‌ای، یک مخزن مستندات (مانند MongoDB) یا یک موتور جستجو (مانند Elasticsearch) را به‌روزرسانی می‌کند.

چالش اصلی، به‌روز نگه داشتن این پروجکشن‌ها بدون افت عملکرد نوشتن است. این موضوع معمولاً از طریق سیستم‌های ناهمگام و سازگار در نهایت (Eventually Consistent) محقق می‌شود.

یک سناریوی سفارش ساده در تجارت الکترونیک را در نظر بگیرید. وقتی کاربر سفارشی ثبت می‌کند، ما یک رویداد OrderCreated منتشر می‌کنیم. یک پردازشگر پروجکشن باید منتظر این رویداد باشد و یک رکورد را برای بازیابی سریع در جدول orders_view درج کند.

class OrderCreatedHandler:
    def handle(self, event: OrderCreated):
        projection_db.insert({
            "order_id": event.order_id,
            "customer_id": event.customer_id,
            "total_amount": event.total,
            "status": "CREATED",
            "created_at": event.timestamp
        })

با این حال، سیستم‌های دنیای واقعی نیاز به مدیریت منطق کسب‌وکار پیچیده دارند. یک رویداد واحد ممکن است چندین پروجکشن را فعال کند. برای مثال، OrderCreated ممکن است داشبورد فروش، سیستم رزرو موجودی و سیستم امتیازات وفاداری مشتری را به‌روزرسانی کند. طراحی این پردازشگرها به گونه‌ای که ایستا (Idempotent) باشند، برای جلوگیری از اثرات جانبی تکراری در طول تلاش مجدد یا شکاف‌های شبکه حیاتی است.

مدیریت تکامل طرحواره: تله غیرقابل تغییر بودن

اصل بنیادین رویداد-محوری این است که یک بار که رویدادی نوشته شد، نمی‌توان آن را تغییر داد. این موضوع یک مشکل منحصر به فرد ایجاد می‌کند: چگونه برنامه خود را زمانی که نیازهای کسب‌وکار تغییر می‌کند، تکامل دهیم؟ اگر یک کلاس را بازطراحی کنیم یا نام یک فیلد را تغییر دهیم، رویدادهای تاریخی به فرمت اصلی خود باقی می‌مانند.

ما نمی‌توانیم به سادگی طرحواره پایگاه داده مخزن رویدادها را به‌روزرسانی کنیم، زیرا رویدادها غیرقابل تغییر هستند. در عوض، باید استراتژی‌ای مبتنی بر نسخه‌بندی و تبدیل را اتخاذ کنیم. دو رویکرد رایج وجود دارد:

  1. تغییرات سازگار با عقب‌گرد: افزودن فیلدهای جدید به رویدادها بدون حذف فیلدهای قدیمی. برای مثال، اگر نیاز به ردیابی یک ارائه‌دهنده حمل‌ونقل جدید داشته باشیم، یک فیلد shipping_provider به رویداد OrderShipped اضافه می‌کنیم. رویدادهای موجود این فیلد را به صورت null خواهند داشت که برنامه به راحتی آن را مدیریت می‌کند.
  2. نسخه‌بندی رویداد: اگر تغییری شکست‌دهنده باشد (مثلاً تغییر کد ارز از "USD" به "US")، باید یک نوع رویداد جدید مانند OrderShippedV2 ایجاد کنیم. سیستم سپس نیاز به منطقی دارد که تعیین کند کدام نسخه از رویداد باید پردازش شود.

در اینجا مثالی از نحوه مدیریت نسخه‌بندی توسط یک پردازشگر آورده شده است:

def process_event(event):
    if event.type == "OrderShipped" and event.version == 1:
        # تبدیل رویداد قدیمی به ساختار فعلی
        normalized_data = migrate_v1_to_current(event.data)
        return save_event(normalized_data)
    elif event.type == "OrderShippedV2":
        return save_event(event.data)
    else:
        raise UnsupportedEventVersionError()

پخش مجدد رویدادها برای سازگاری

تکامل طرحواره تنها درباره خواندن داده‌های جدید نیست؛ بلکه اغلب درباره اصلاح داده‌های قدیمی است. اگر باگی در منطق پروجکشن خود معرفی کنید یا نیاز به افزودن یک فیلد جدید به مدل خواندنی خود داشته باشید، می‌توانید به سادگی پردازشگرهای پروجکشن خود را روی جریان رویداد موجود مجدداً اجرا کنید.

قابلیت «پخش مجدد» این، ابرقدرت رویداد-محوری است. این امکان را به شما می‌دهد تا کل پایگاه داده خواندنی خود را از نو بسازید تا مطمئن شوید که کاملاً با طرحواره فعلی مطابقت دارد.

def rebuild_projection(event_store, projection_engine):
    # 1. پاک کردن مخزن پروجکشن موجود
    projection_engine.clear()
    
    # 2. جریان تمام رویدادهای تاریخی
    events = event_store.get_all_events()
    
    # 3. پردازش مجدد آن‌ها از طریق منطق جدیدترین پردازشگر
    for event in events:
        projection_engine.process(event)

نتیجه‌گیری

مدل‌سازی داده در رویداد-محوری نیازمند تغییر نگرش از عملیات سنتی CRUD است. شما دیگر تنها در حال طراحی جداول نیستید؛ بلکه در حال طراحی یک خط زمانی از حقایق هستید. با تمرکز بر ساختارهای رویداد غیرقابل تغییر، پیاده‌سازی استراتژی‌های نسخه‌بندی مقاوم و بهره‌گیری از قدرت پخش مجدد رویدادها، می‌توانید سیستم‌های توزیع‌شده‌ای بسازید که نه تنها مقاوم، بلکه سازگار با نیازهای در حال تغییر کسب‌وکار هستند.

نکته کلیدی این است که تکامل طرحواره یک شهروند درجه یک در رویداد-محوری است. غیرقابل تغییر بودن رویدادها را بپذیرید، پروجکشن‌های انعطاف‌پذیر طراحی کنید و پتانسیل کامل این الگوی معماری قدرتمند را آزاد کنید.

Share: