مرحبا هبر! قررت الابتعاد عن Scala و Idris وغيرهما من FP لفترة من الوقت والتحدث قليلاً عن Event Store - قاعدة بيانات يمكن من خلالها حفظ الأحداث في تدفقات الأحداث. كما في الكتاب القديم الجيد ، لدينا أيضًا الفرسان في الحقيقة 4 والرابع هو DDD. أولاً ، أستخدم Event Storming لتحديد الأوامر والأحداث والكيانات المرتبطة بها. ثم ، على أساسها ، سأحفظ حالة الكائن وأستعيده. في هذه المقالة ، سأقوم بعمل قائمة تودو عادية. لمزيد من التفاصيل ، مرحبا بكم تحت القط.
المحتوى
- الفرسان الثلاثة - مصادر الأحداث ، اقتحام الأحداث ، ومتجر الأحداث - أدخل المعركة: الجزء 1 - تجربة متجر أحداث DB
الروابط
مصادر
الصور docker image
Event Store
Event Soucing
Event Storming
في الواقع ، يعد Event Store قاعدة بيانات مصممة لتخزين الأحداث. وهي تعرف أيضًا كيفية إنشاء اشتراكات للأحداث بحيث يمكن معالجتها بطريقة ما. هناك أيضًا توقعات تتفاعل أيضًا مع الأحداث وتراكم بعض البيانات على أساسها. على سبيل المثال ، أثناء حدث TodoCreated ، يمكنك زيادة نوع من عداد العد في الإسقاط. في الوقت الحالي ، في هذا الجزء سأستخدم متجر الأحداث كقراءة وكتابة ديسيبل. علاوة على ذلك ، في المقالات التالية ، سوف أقوم بإنشاء قاعدة بيانات منفصلة للقراءة فيها سيتم كتابة البيانات بناءً على الأحداث المحفوظة في قاعدة البيانات لكتابتها في متجر الأحداث. سيكون هناك أيضًا مثال على كيفية القيام بـ "السفر عبر الزمن" عن طريق إعادة النظام إلى الحالة التي كان عليها في الماضي.
ولذا فلنبدأ Event Stroming. عادة ، من أجل تنفيذه ، يتم جمع جميع الأشخاص المهتمين والخبراء الذين يخبرون الأحداث في مجال الموضوع التي سيحاكيها البرنامج. على سبيل المثال ، لبرامج المصنع - ProductManufactured. للعبة - الضرر الحاصل. بالنسبة للبرامج المالية - يتم إيداع الأموال في الحساب وما إلى ذلك. نظرًا لأن مجال موضوعنا بسيط مثل TodoList ، سيكون لدينا عدد قليل من الأحداث. وهكذا ، دعنا نكتب أحداث مجال موضوعنا (المجال) على السبورة.
الآن دعنا نضيف الأوامر التي تطلق هذه الأحداث.
بعد ذلك ، دعنا نجمع هذه الأحداث والأوامر حول الكيان مع تغيير في الحالة المرتبطة بها.
ستتحول أوامري ببساطة إلى أسماء طرق الخدمة. دعنا ننتقل إلى التنفيذ.
أولاً ، دعنا نصف الأحداث في الكود.
public interface IDomainEvent
{
// . id Event Strore
Guid EventId { get; }
// . Event Store
long EventNumber { get; set; }
}
public sealed class TodoCreated : IDomainEvent
{
//Id Todo
public Guid Id { get; set; }
// Todo
public string Name { get; set; }
public Guid EventId => Id;
public long EventNumber { get; set; }
}
public sealed class TodoRemoved : IDomainEvent
{
public Guid EventId { get; set; }
public long EventNumber { get; set; }
}
public sealed class TodoCompleted: IDomainEvent
{
public Guid EventId { get; set; }
public long EventNumber { get; set; }
}
الآن جوهرنا كيان:
public sealed class Todo : IEntity<TodoId>
{
private readonly List<IDomainEvent> _events;
public static Todo CreateFrom(string name)
{
var id = Guid.NewGuid();
var e = new List<IDomainEvent>(){new TodoCreated()
{
Id = id,
Name = name
}};
return new Todo(new TodoId(id), e, name, false);
}
public static Todo CreateFrom(IEnumerable<IDomainEvent> events)
{
var id = Guid.Empty;
var name = String.Empty;
var completed = false;
var ordered = events.OrderBy(e => e.EventNumber).ToList();
if (ordered.Count == 0)
return null;
foreach (var @event in ordered)
{
switch (@event)
{
case TodoRemoved _:
return null;
case TodoCreated created:
name = created.Name;
id = created.Id;
break;
case TodoCompleted _:
completed = true;
break;
default: break;
}
}
if (id == default)
return null;
return new Todo(new TodoId(id), new List<IDomainEvent>(), name, completed);
}
private Todo(TodoId id, List<IDomainEvent> events, string name, bool isCompleted)
{
Id = id;
_events = events;
Name = name;
IsCompleted = isCompleted;
Validate();
}
public TodoId Id { get; }
public IReadOnlyList<IDomainEvent> Events => _events;
public string Name { get; }
public bool IsCompleted { get; private set; }
public void Complete()
{
if (!IsCompleted)
{
IsCompleted = true;
_events.Add(new TodoCompleted()
{
EventId = Guid.NewGuid()
});
}
}
public void Delete()
{
_events.Add(new TodoRemoved()
{
EventId = Guid.NewGuid()
});
}
private void Validate()
{
if (Events == null)
throw new ApplicationException(" ");
if (string.IsNullOrWhiteSpace(Name))
throw new ApplicationException(" ");
if (Id == default)
throw new ApplicationException(" ");
}
}
نحن نتصل بمتجر الأحداث:
services.AddSingleton(sp =>
{
// TCP .
// . .
var con = EventStoreConnection.Create(new Uri("tcp://admin:changeit@127.0.0.1:1113"), "TodosConnection");
con.ConnectAsync().Wait();
return con;
});
وهكذا ، الجزء الرئيسي. تخزين الأحداث وقراءتها من متجر الأحداث نفسه:
public sealed class EventsRepository : IEventsRepository
{
private readonly IEventStoreConnection _connection;
public EventsRepository(IEventStoreConnection connection)
{
_connection = connection;
}
public async Task<long> Add(Guid collectionId, IEnumerable<IDomainEvent> events)
{
var eventPayload = events.Select(e => new EventData(
//Id
e.EventId,
//
e.GetType().Name,
// Json (True|False)
true,
//
Encoding.UTF8.GetBytes(JsonSerializer.Serialize((object)e)),
//
Encoding.UTF8.GetBytes((string)e.GetType().FullName)
));
//
var res = await _connection.AppendToStreamAsync(collectionId.ToString(), ExpectedVersion.Any, eventPayload);
return res.NextExpectedVersion;
}
public async Task<List<IDomainEvent>> Get(Guid collectionId)
{
var results = new List<IDomainEvent>();
long start = 0L;
while (true)
{
var events = await _connection.ReadStreamEventsForwardAsync(collectionId.ToString(), start, 4096, false);
if (events.Status != SliceReadStatus.Success)
return results;
results.AddRange(Deserialize(events.Events));
if (events.IsEndOfStream)
return results;
start = events.NextEventNumber;
}
}
public async Task<List<T>> GetAll<T>() where T : IDomainEvent
{
var results = new List<IDomainEvent>();
Position start = Position.Start;
while (true)
{
var events = await _connection.ReadAllEventsForwardAsync(start, 4096, false);
results.AddRange(Deserialize(events.Events.Where(e => e.Event.EventType == typeof(T).Name)));
if (events.IsEndOfStream)
return results.OfType<T>().ToList();
start = events.NextPosition;
}
}
private List<IDomainEvent> Deserialize(IEnumerable<ResolvedEvent> events) =>
events
.Where(e => IsEvent(e.Event.EventType))
.Select(e =>
{
var result = (IDomainEvent)JsonSerializer.Deserialize(e.Event.Data, ToType(e.Event.EventType));
result.EventNumber = e.Event.EventNumber;
return result;
})
.ToList();
private static bool IsEvent(string eventName)
{
return eventName switch
{
nameof(TodoCreated) => true,
nameof(TodoCompleted) => true,
nameof(TodoRemoved) => true,
_ => false
};
}
private static Type ToType(string eventName)
{
return eventName switch
{
nameof(TodoCreated) => typeof(TodoCreated),
nameof(TodoCompleted) => typeof(TodoCompleted),
nameof(TodoRemoved) => typeof(TodoRemoved),
_ => throw new NotImplementedException(eventName)
};
}
}
يبدو متجر الكيانات بسيطًا جدًا. نحصل على أحداث الكيان من EventStore ونستعيدها منها ، أو نقوم ببساطة بحفظ أحداث الكيان.
public sealed class TodoRepository : ITodoRepository
{
private readonly IEventsRepository _eventsRepository;
public TodoRepository(IEventsRepository eventsRepository)
{
_eventsRepository = eventsRepository;
}
public Task SaveAsync(Todo entity) => _eventsRepository.Add(entity.Id.Value, entity.Events);
public async Task<Todo> GetAsync(TodoId id)
{
var events = await _eventsRepository.Get(id.Value);
return Todo.CreateFrom(events);
}
public async Task<List<Todo>> GetAllAsync()
{
var events = await _eventsRepository.GetAll<TodoCreated>();
var res = await Task.WhenAll(events.Where(t => t != null).Where(e => e.Id != default).Select(e => GetAsync(new TodoId(e.Id))));
return res.Where(t => t != null).ToList();
}
}
الخدمة التي يتم فيها العمل مع المستودع والجهة:
public sealed class TodoService : ITodoService
{
private readonly ITodoRepository _repository;
public TodoService(ITodoRepository repository)
{
_repository = repository;
}
public async Task<TodoId> Create(TodoCreateDto dto)
{
var todo = Todo.CreateFrom(dto.Name);
await _repository.SaveAsync(todo);
return todo.Id;
}
public async Task Complete(TodoId id)
{
var todo = await _repository.GetAsync(id);
todo.Complete();
await _repository.SaveAsync(todo);
}
public async Task Remove(TodoId id)
{
var todo = await _repository.GetAsync(id);
todo.Delete();
await _repository.SaveAsync(todo);
}
public async Task<List<TodoReadDto>> GetAll()
{
var todos = await _repository.GetAllAsync();
return todos.Select(t => new TodoReadDto()
{
Id = t.Id.Value,
Name = t.Name,
IsComplete = t.IsCompleted
}).ToList();
}
public async Task<List<TodoReadDto>> Get(IEnumerable<TodoId> ids)
{
var todos = await Task.WhenAll(ids.Select(i => _repository.GetAsync(i)));
return todos.Where(t => t != null).Select(t => new TodoReadDto()
{
Id = t.Id.Value,
Name = t.Name,
IsComplete = t.IsCompleted
}).ToList();
}
}
حسنًا ، في الواقع ، لا شيء مثير للإعجاب حتى الآن. في المقالة التالية ، عندما أقوم بإضافة قاعدة بيانات منفصلة للقراءة ، فإن كل شيء سوف يتألق بألوان مختلفة. سيؤدي هذا إلى تعليق التناسق على الفور مع مرور الوقت. مخزن الأحداث و SQL DB على مبدأ السيد - العبد. واحد ES أبيض والعديد من MS SQL السوداء التي تقرأ منها البيانات.
استطرادا غنائي. في ضوء الأحداث الأخيرة ، لم يسعني إلا أن أمزح عن السيد العبد والبيض السود. إيه ، العصر يغادر ، سنخبر أحفادنا أننا عشنا في وقت كانت القواعد أثناء التكرار تسمى السيد والعبد.
في الأنظمة التي يوجد بها الكثير من القراءة والقليل من كتابة البيانات (معظمها) ، سيؤدي ذلك إلى زيادة سرعة العمل. في الواقع ، فإن النسخ المتماثل للسيد العبد نفسه ، يهدف إلى حقيقة أن كتابتك تتباطأ (كما هو الحال مع الفهارس) ، ولكن في المقابل ، يتم تسريع القراءة عن طريق توزيع الحمل عبر عدة قواعد بيانات.