Halo, Habr! Saya memutuskan untuk menjauh dari Scala, Idris, dan FP lainnya untuk sementara waktu dan berbicara sedikit tentang Toko Acara - database tempat acara dapat disimpan ke aliran acara. Seperti di buku lama yang bagus, kami juga memiliki Musketeers pada kenyataannya 4 dan yang keempat adalah DDD. Pertama, saya menggunakan Event Storming untuk memilih perintah, acara, dan entitas yang terkait dengannya. Kemudian, atas dasar mereka, saya akan menyimpan status objek dan memulihkannya. Saya akan membuat TodoList biasa di artikel ini. Untuk detailnya, selamat datang di bawah cat.
Kandungan
- The Three Musketeers - Sumber Acara, Penyerangan Acara, dan Toko Acara - Masuki Pertempuran: Bagian 1 - Mencoba Toko Acara DB
Tautan
Sumber
Gambar gambar buruh pelabuhan
Event Store
Event Soucing
Event Storming
Sebenarnya Event Store adalah database yang didesain untuk menyimpan event. Dia juga tahu cara membuat langganan ke acara sehingga acara itu bisa diproses. Ada juga proyeksi yang juga bereaksi terhadap peristiwa dan, atas dasarnya, mengumpulkan beberapa data. Misalnya, selama acara TodoCreated, Anda dapat meningkatkan penghitung Penghitung dalam proyeksi. Untuk saat ini, di bagian ini saya akan menggunakan Event Store sebagai Baca dan Tulis Db. Selanjutnya dalam artikel berikut saya akan membuat database terpisah untuk membaca data yang akan ditulis berdasarkan peristiwa yang disimpan dalam database untuk ditulis ke Event Store. Juga akan ada contoh bagaimana melakukan "Perjalanan Waktu" dengan mengembalikan sistem ke keadaan sebelumnya.
Jadi mari kita mulai Event Stroming. Biasanya, untuk implementasinya, semua orang yang tertarik dan ahli dikumpulkan yang menceritakan peristiwa apa di bidang subjek yang akan disimulasikan oleh perangkat lunak. Misalnya, untuk perangkat lunak pabrik - Produk yang Diproduksi. Untuk game - Kerusakan telah terjadi. Untuk Software Keuangan - Uang dikreditkan ke Akun dan sebagainya. Karena subjek kami sesederhana TodoList, kami hanya akan mengadakan sedikit acara. Jadi, mari kita tulis peristiwa di bidang subjek (domain) kita di papan tulis.
Sekarang mari tambahkan perintah yang memicu kejadian ini.
Selanjutnya, mari kelompokkan peristiwa dan perintah ini di sekitar entitas dengan perubahan status terkait.
Perintah saya hanya akan berubah menjadi nama metode layanan. Mari kita mulai implementasi.
Pertama, mari kita gambarkan Peristiwa dalam kode.
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; }
}
Sekarang inti kita adalah sebuah entitas:
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(" ");
}
}
Kami terhubung ke Toko Acara:
services.AddSingleton(sp =>
{
// TCP .
// . .
var con = EventStoreConnection.Create(new Uri("tcp://admin:changeit@127.0.0.1:1113"), "TodosConnection");
con.ConnectAsync().Wait();
return con;
});
Jadi, bagian utamanya. Menyimpan dan membaca acara dari Toko Acara itu sendiri:
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)
};
}
}
Toko entitas terlihat sangat sederhana. Kami mendapatkan acara entitas dari EventStore dan memulihkannya dari mereka, atau kami cukup menyimpan acara entitas.
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();
}
}
Layanan tempat pekerjaan dengan repositori dan entitas berlangsung:
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();
}
}
Sebenarnya, sejauh ini tidak ada yang mengesankan. Di artikel selanjutnya, ketika saya menambahkan database terpisah untuk membaca, semuanya akan berkilau dengan warna berbeda. Ini akan segera menggantungkan konsistensi kita dari waktu ke waktu. Event Store dan SQL DB dengan prinsip master - slave. Satu ES putih dan banyak MS SQL hitam tempat membaca data.
Penyimpangan lirik. Mengingat kejadian baru-baru ini, saya tidak dapat menahan diri untuk tidak bercanda tentang tuan budak dan kulit putih hitam. Ehe, jaman akan pergi, kami akan memberitahu cucu kami bahwa kami hidup pada saat pangkalan selama replikasi disebut tuan dan budak.
Dalam sistem di mana ada banyak pembacaan dan sedikit penulisan data (kebanyakan dari mereka), ini akan meningkatkan kecepatan kerja. Sebenarnya, replikasi master slave itu sendiri, ini ditujukan pada fakta bahwa penulisan Anda melambat (seperti pada indeks), tetapi sebaliknya, pembacaan dipercepat dengan mendistribusikan beban ke beberapa database.