آموزش Event Sourcing بخش هفتم
سلام دنیا به روش Event Sourcing- Projection
لیست مطالب آموزشی Event Sourcing:
- بخش اول مقدمهای بر Event Sourcing
- بخش دوم آشنایی مقدماتی با ساختار داخلی Event Store
- مقایسه رویکردهای State-Oriented و State-Transition
- مزیتهای Event Sourcing
- سلام به دنیا به روش Event Sourcing
- سلام به دنیا به روش Event Sourcing-بخش دوم
- بخش هفتم Projection
- ویرایش event ها در EventSourcing
- Message، Command یا Event، کدوم رو انتخاب کنم؟
در بخش پنجم و بخش ششم با ارائه مثال ساده حساب بانکی، به پیادهسازی مثال حساب بانکی به روش ایونت سورسینگ پرداختیم. در این قسمت به معرفی مقدماتی فرآیند Projection در Event Sourcing خواهم پرداخت.
مقدمه
اگر به ویکیپدیا نگاهی بندازیم، Projection در فیزیک بصورت زیر تعریف شده است:
“The action/process of light, heat, or sound reflecting from a surface to another in a different direction.”
Projection در فیزیک انعکاس و باز نمایش-representation یک چیزی مثل نور، یا صدا و تصویر با جایی دیگر است. مثل وقتی که تصویر چیزی را در آب میبینیم. یا فیلم یا تصویری رو روی یک پرده مشاهده میکنیم. در اینحالت تصویر یا فیلم بر روی سطح دیگری project شده است. در این فرآیند projection ممکن است سوژه ما دچار تغییر و تحول شود. مثلا در مورد تصویر، ممکن است که ابعاد تصویر بزرگتر یا کوچکتر شود. در مورد صدا، علاوه بر اینکه زیر و بمی صدا ممکن است تغییر کند، جهت صدا نیز به سمت ما تغییر میکند. طبیعی هم است. فرض کنید بخواهیم یک پرزنت را برای ۲۰ نفر انجام دهیم. ترجیح میدهیم که تصویر پرزنت ما از طریق لپتاپ بر روی یک مانیتور بزرگتر یا پردهی بزرگتر روی دیوار project شود.
اما Projector اون کسی یا چیزی است که این عملیات projection را انجام میدهد. در مثال فیلم یا تصویر، ویدئو پروجکتور این عمل را برای ما انجام میدهد. یا اگر در کوهی داد بزنیم، این کوه است که به عنوان projector عمل میکند و صدای ما را به سمت خودمان project میکند.
منبع تصویر: https://en.wikipedia.org/wiki/Projector#/media/File:DLP_Home_Theatre_Projector.jpg
Projection در Event Sourcing
Event مهمترین بلاک سازنده در Event Sourcing است. Projection در Event Sourcing هم اشاره به project کردن event ها دارد. این eventها حقایقی هستند که در دومین مورد نظر ما رخ داده اند، و بصورت یک event به ثبت رسیدهاند. Event شبیه یک فریم مشخص در یک زمان مشخص از یک فیلم است. فیلم در اینجا دومین ما است. به عنوان مثال این حقیقت که: مسعود بهرامی در ساعت ۱۴:۴۵ عصر در تاریخ ۲۵ خرداد ۱۴۰۲ یک سفارش را به ثبت رساند، یک فریم خاص از تمامی حقایقی است که در مورد این دامین و ثبت سفارش وجود دارد.
در اصل این eventها یک نمایش-representation از حقایقی هستند که در دومین رخداده اند. همانطور که در پستهای مشاهده کردید در این نوع representation از دومین، ما وضعیت هر ریسورس در دومین را بصورت دنبالهای از حقایق رکورد میکنیم. همانند فیلم که باید تمامی فریمهای آن پشت سر هم اجرا بشوند برای اینکه آخرین وضعیت آن ریسورس را داشته باشیم، نیاز داریم که این representation که در قالب event هستند، را پشت سر هم اجرا کنیم. این نوع طراحی برای سناریوهایی مناسب است.
چرا به Projection در Event Sourcing نیاز داریم
این نوع representation اگرچه بلاک اصلی سازنده Event Sourcing است، اما برای تمامی سناریوها مناسب نیست. به عنوان مثال فرض کنید که آخرین وضعیت یک سفارش را برای یک مشتری نیاز داریم. ممکن است اتفاقات بسیار زیادی در دومین بر روی این سفارش انجام شود. بارها و بارها کالاهای مختلف به سفارش اضافه یا کم بشود. وضعیت سفارش تغییر کند. در این حالت برای نمایش آخرین وضعیت سفارش به کاربر خیلی مقرون به صرفه نیست که هر دفعه مجبور باشیم تمامی eventهای آن سفارش رو لود کرده و همانند تصویر بالا پلی کنیم، تا آخرین وضعیت سفارش را بدست آوریم.
نکتهی دیگری که باعث میشود representation که در بالا بدان اشاره کردیم، همیشه بهترین انتخاب نباشد، این است که کاردینالیتی و diversity نمایش اطلاعات در کوئری ساید بسیار بیشتر از تغییرات در وضعیت برنامه است. به عنوان مثال در مورد آیتمهای موجود در یک سفارش، نیاز است که اسم کالا، تصویر، قیمت و سایر مشخصات آن کالا را نیز به کاربر نمایش دهیم. در صورتی که این اطلاعات در event مربوط به ثبت سفارش یا event مربوط به افزودن یا حذف کردن کالایی به آن سفارش وجود ندارد.
برای اینکه بهتر متوجه چالش بشویم، فرض کنیم قصد داریم در back office لیست تمامی سفارشهای امروز را به کاربر نمایش دهیم.
فرض کنید برای هر سفارش ۱۰ event ثبت شده است. Eventها میتواند شامل: سفارشی ثبت شده است، کالایی به سفارش اضافه یا حذف شده است و … باشد.
اگر فرض کنید ۴۰۰ سفارش برای امروز به ثبت رسیده باشد، میتوانیم مشاهده کنید که لود کردن ۴۰۰۰ ایونت و سپس اپلای کردن آنها بر روی ۴۰۰ instance از سفارش خیلی بهینه و مهمتر ساده نیست. قضیه وقتی سختتر میشود که مثلا بخواهید criteriaها هرچند ساده رو بر روی این سفارشها ران کنید. مثلا سفارشهایی که شامل گوشی ایفون ۱۴ هستند. مشخص است که این روش به هیچ وجه saleable نمیباشد.
Projection یک نمایش-representation دیگر از حقایق رخداده شده در دومین است. به عبارت دیگر حقایق به وقوع پیوسته در دومین که بصورت event رکورد شدهاند را قصد داریم بصورت دیگر نمایش بدهیم.
Projection یک نمایش-representation دیگر از حقایق رخداده شده در دومین است. به عبارت دیگر حقایق به وقوع پیوسته در دومین که بصورت event رکورد شدهاند را قصد داریم بصورت دیگر نمایش بدهیم.
پیادهسازی Projection
در این پست از مثال سفارش استفاده کردم. در این بخش پیادهسازی ساده از Projection در سفارش مشتری انجام میدهم. فرض کنید eventهای OrderIsPlaced و ItemIsAddedToOrder، ItemIsRemovedFromOrder و OrderIsPaid در دومین وجود دارد.
کلاسهای مربوط به این eventها را در زیر میتوانید مشاهده کنید.
public class OrderEvents
{
public class OrderIsPlaced : IsADomainEvent
{
public DateTime On { get; }
public string Owner { get; }
public OrderIsPlaced(string orderId, DateTime on, string owner) : base(orderId)
{
On = on;
Owner = owner;
}
public override IEnumerable<object> GetEqualityComponents()
{
yield return AggregateId;
}
}
public class ItemIsAddedToOrder : IsADomainEvent
{
public string ProductId { get; }
public ItemIsAddedToOrder(string orderId, string productId) : base(orderId)
{
ProductId = productId;
}
public override IEnumerable<object> GetEqualityComponents()
{
yield return AggregateId;
}
}
public class ItemIsRemovedFromOrder : IsADomainEvent
{
public string ProductId { get; }
public ItemIsRemovedFromOrder(string orderId , string productId) : base(orderId)
{
ProductId = productId;
}
public override IEnumerable<object> GetEqualityComponents()
{
yield return AggregateId;
}
}
public class OrderIsPaid : IsADomainEvent
{
public OrderIsPaid(string orderId) : base(orderId)
{
}
public override IEnumerable<object> GetEqualityComponents()
{
yield return AggregateId;
}
}
}
Representation جدید برای سفارش OrderViewModel است. این همان مدلی است که قصد داریم eventهای بالا را به آن project کنیم. پیادهسازی این کلاس در زیر قابل مشاهده است.
public class OrderViewModel
{
public string OrderId { get; set; }
public string Owner { get; set; }
public DateTime IssuedOn { get; set; }
public OrderStatus Status{ get; set; }
public List<OrderLineItem> LineItems { get; set; }
}
public class OrderLineItem
{
public string ProductId { get; set; }
}
public enum OrderStatus
{
Issued,
InProgress,
Paid
}
پروجکتور شبیه یک transformer عمل میکند. بدین معنی که با دریافت یک event جدید و داشتن وضعیت جاری view model، آن view model را به وضعیت جدید میبرد.
public class OrderProjection : ImAProjector
{
private readonly DbContext _dbContext;
public OrderProjection(DbContext dbContext) => _dbContext = dbContext;
public override DbOperationCommand Transform(IsADomainEvent @event)
=> When((dynamic)@event);
private DbUpdateOperation<OrderViewModel> When(OrderEvents.ItemIsAddedToOrder @event)
=> new DbUpdateOperation<OrderViewModel>(_dbContext, o=>o.OrderId == @event.AggregateId)
.With(order =>
{
order.LineItems.Add(new OrderLineItem{ProductId = @event.ProductId});
});
private DbUpdateOperation<OrderViewModel> When(OrderEvents.ItemIsRemovedFromOrder @event)
=> new DbUpdateOperation<OrderViewModel>(_dbContext, o => o.OrderId == @event.AggregateId)
.With(order =>
{
order.LineItems.Remove(new OrderLineItem { ProductId = @event.ProductId });
});
private DbUpdateOperation<OrderViewModel> When(OrderEvents.OrderIsPlaced @event)
=> new DbUpdateOperation<OrderViewModel>(_dbContext, o => o.OrderId == @event.AggregateId)
.With(order =>
{
order.Status = OrderStatus.Paid;
});
}
مهمترین متد در پروجکتور بالا متد Transform است. سپس پروجکتور به ازای eventهای سفارش متد When متناسب را دارد. هر متد When ریاکشن مناسب را در قبال project کردن آن رویداد مشخص انجام میدهد. به عنوان وقتی رویداد OrderIsPaid دریافت میشود، وضعیت OrderViewModel نیز به وضعیت جدید تغییر خواهد کرد. نکتهی مهم در اینجا این است که ما همیشه آخرین وضعیت رو نگه میداریم. و وضعیتهای قبلی را overwrite میکنیم. این representation از سفارش در اصل یک کش از آخرین وضعیت سفارش را نگهداری میکند.
جهت سادگی مثال، order view model را با کمترین پراپرتیهای مورد نظر پیادهسازی کردم.
پایان بخش هفتم