Ответ
Event Sourcing (Источники событий) — это архитектурный паттерн, при котором состояние приложения определяется не как последний «снимок» данных, а как последовательность всех событий (история изменений), которые привели к этому состоянию. Вместо обновления записи в БД каждое изменение сохраняется как неизменяемое событие (event). Текущее состояние вычисляется путём применения (проецирования) всей цепочки событий.
Ключевые принципы:
- Неизменяемость событий: События, будучи сохранёнными, никогда не изменяются и не удаляются.
- Единственный источник истины: Журнал событий (event log) является первичным и наиболее достоверным хранилищем данных.
- Воспроизводимость: Любое состояние может быть восстановлено «с нуля» путём повторного применения событий, что обеспечивает мощные возможности для отладки, аудита и миграции.
Преимущества:
- Полный аудит: Вся история изменений доступна по умолчанию.
- Временные запросы: Можно «отмотать» состояние приложения к любому моменту в прошлом.
- Гибкость: Новые представления данных (проекции) могут быть созданы из существующего потока событий без изменения основной логики.
- Согласованность: Упрощает реализацию сложных бизнес-транзакций и реакцию на изменения.
Недостатки и сложности:
- Сложность запросов: Прямые запросы к журналу событий неэффективны. Паттерн почти всегда используется в связке с CQRS (Command Query Responsibility Segregation), где для чтений создаются оптимизированные проекции (read models).
- Рост данных: Требует стратегий для управления постоянно растущим журналом событий (снепшоты, архивация).
- Сложность освоения: Требует сдвига парадигмы в мышлении о данных.
Пример на C# (Упрощённая модель банковского счёта):
// 1. Определяем события (неизменяемые объекты-записи)
public abstract record AccountEvent(Guid AccountId, DateTime OccurredAt);
public record AccountOpened(Guid AccountId, string Owner, DateTime OccurredAt) : AccountEvent(AccountId, OccurredAt);
public record MoneyDeposited(Guid AccountId, decimal Amount, DateTime OccurredAt) : AccountEvent(AccountId, OccurredAt);
public record MoneyWithdrawn(Guid AccountId, decimal Amount, DateTime OccurredAt) : AccountEvent(AccountId, OccurredAt);
// 2. Агрегат, который обрабатывает команды и генерирует события
public class BankAccount
{
public Guid Id { get; private set; }
public string Owner { get; private set; } = string.Empty;
public decimal Balance { get; private set; }
private readonly List<AccountEvent> _pendingEvents = new();
// Восстановление состояния из истории событий
public BankAccount(IEnumerable<AccountEvent> pastEvents)
{
foreach (var evt in pastEvents)
Apply(evt);
}
// Метод для применения события (изменяет состояние)
private void Apply(AccountEvent evt)
{
switch (evt)
{
case AccountOpened opened:
Id = opened.AccountId;
Owner = opened.Owner;
Balance = 0m;
break;
case MoneyDeposited deposited:
Balance += deposited.Amount;
break;
case MoneyWithdrawn withdrawn:
Balance -= withdrawn.Amount;
break;
}
}
// Команды: проверяют бизнес-правила и создают новые события
public void Open(string owner)
{
if (Id != Guid.Empty) throw new InvalidOperationException("Account already open.");
var evt = new AccountOpened(Guid.NewGuid(), owner, DateTime.UtcNow);
_pendingEvents.Add(evt);
Apply(evt);
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Deposit amount must be positive.");
var evt = new MoneyDeposited(Id, amount, DateTime.UtcNow);
_pendingEvents.Add(evt);
Apply(evt);
}
// Возвращает новые, ещё не сохранённые события
public IReadOnlyList<AccountEvent> GetPendingEvents() => _pendingEvents.AsReadOnly();
public void ClearPendingEvents() => _pendingEvents.Clear();
}
// 3. Использование
// Восстановление счёта из хранилища событий
var pastEvents = eventStore.LoadEvents(accountId);
var account = new BankAccount(pastEvents);
// Выполнение новой команды
account.Deposit(100);
// Сохранение новых событий
var newEvents = account.GetPendingEvents();
eventStore.SaveEvents(account.Id, newEvents);
account.ClearPendingEvents(); Ответ 18+ 🔞
А, Event Sourcing, говоришь? Ну это такая штука, когда ты не хранишь последнюю версию данных, как все нормальные люди, а сохраняешь каждое изменение как отдельное, блядь, событие. Представь, что у тебя не просто запись "на счету 100 рублей", а целая история: "открыл счет", "положил 50", "снял 20", "положил 70". И чтобы узнать текущий баланс, надо всю эту хуйню просуммировать. Звучит как овердохуища работы, да? Но зато ты можешь в любой момент посмотреть, а что там было вчера в 15:30, и кто последний мудак снял все деньги.
В чём соль, блядь?
- События — это святое. Раз записали — нихуя не трогаем. Никаких "исправил", "удалил". История должна быть честной, как совесть Герасима после Муму.
- Журнал событий — царь и бог. Это главная правда, всё остальное — производное.
- Можно отмотать назад. Как в контроллере, когда проёбываешь уровень. Хочешь посмотреть, как всё было до того, как твой коллега накосячил с миграцией? Пожалуйста, откати события и смотри. Удобно, ёпта.
Что хорошего?
- Аудит на блюдечке. Кто, что и когда сделал — всё записано. Идеально, когда начальство спрашивает "а с какого хуя тут минус?".
- Машина времени. Состояние на любую дату — не проблема.
- Гибкость ебать. Позже можно из тех же событий нагенерировать новые отчёты или дашборды, о которых изначально и не думал.
- Согласованность. Сложные бизнес-процессы становится проще описывать, потому что каждое изменение — это событие, на которое можно подписаться.
А где подвох?
- Запросы нихуя не удобные. Спросить у журнала "покажи все счета, где баланс больше нуля" — это пиздец. Поэтому эту тему почти всегда используют с CQRS: пишем в один стрим событий, а для чтений держим отдельную, оптимизированную под запросы, копию данных (проекцию). Две работы, зато быстро.
- Данных становится дохуя. Событий копится, как говна в хлеву. Нужно думать про снепшоты (сохранять состояние на определённый момент, чтобы не пересчитывать всё с нуля) и архивацию.
- Голову сломать можно. Пока перестроишь мозги с "обновляю запись" на "генерирую событие", можешь поседеть. Это не для простых CRUD-приложений, где "сохранил форму и пошёл пить чай".
Ну и примерчик, чтобы не на словах (C#):
// 1. События. Просто данные, которые уже случились. Неизменяемые.
public abstract record AccountEvent(Guid AccountId, DateTime OccurredAt);
public record AccountOpened(Guid AccountId, string Owner, DateTime OccurredAt) : AccountEvent(AccountId, OccurredAt);
public record MoneyDeposited(Guid AccountId, decimal Amount, DateTime OccurredAt) : AccountEvent(AccountId, OccurredAt);
public record MoneyWithdrawn(Guid AccountId, decimal Amount, DateTime OccurredAt) : AccountEvent(AccountId, OccurredAt);
// 2. Агрегат (наш банковский счёт). Он знает правила и порождает события.
public class BankAccount
{
public Guid Id { get; private set; }
public string Owner { get; private set; } = string.Empty;
public decimal Balance { get; private set; }
private readonly List<AccountEvent> _pendingEvents = new(); // События, которые ещё не сохранили
// Восстанавливаем состояние, проигрывая историю событий
public BankAccount(IEnumerable<AccountEvent> pastEvents)
{
foreach (var evt in pastEvents)
Apply(evt); // Применяем каждое старое событие к себе
}
// Применяем событие, меняя своё внутреннее состояние
private void Apply(AccountEvent evt)
{
switch (evt)
{
case AccountOpened opened:
Id = opened.AccountId;
Owner = opened.Owner;
Balance = 0m;
break;
case MoneyDeposited deposited:
Balance += deposited.Amount;
break;
case MoneyWithdrawn withdrawn:
Balance -= withdrawn.Amount; // Надеемся, что не уйдёт в минус, но тут надо проверки добавить
break;
}
}
// Команда "Открыть счёт". Проверяем правила и создаём событие.
public void Open(string owner)
{
if (Id != Guid.Empty) throw new InvalidOperationException("Account already open.");
var evt = new AccountOpened(Guid.NewGuid(), owner, DateTime.UtcNow);
_pendingEvents.Add(evt); // Запоминаем новое событие
Apply(evt); // И сразу применяем к себе, чтобы состояние было актуальным
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Deposit amount must be positive.");
var evt = new MoneyDeposited(Id, amount, DateTime.UtcNow);
_pendingEvents.Add(evt);
Apply(evt);
}
// Отдаём накопившиеся события наружу, чтобы их сохранили
public IReadOnlyList<AccountEvent> GetPendingEvents() => _pendingEvents.AsReadOnly();
public void ClearPendingEvents() => _pendingEvents.Clear();
}
// 3. Как этим пользоваться в жизни
// Достаём из хранилища все прошлые события для счёта
var pastEvents = eventStore.LoadEvents(accountId);
// Воссоздаём объект счёта, проиграв историю
var account = new BankAccount(pastEvents);
// Делаем что-то новое
account.Deposit(100);
// Забираем новые, ещё не сохранённые события
var newEvents = account.GetPendingEvents();
// Пишем их в постоянное хранилище (тот самый event log)
eventStore.SaveEvents(account.Id, newEvents);
// Чистим список ожидающих событий
account.ClearPendingEvents();
Короче, паттерн мощный, но это как кувалдой гвозди забивать. Для простых задач — оверкилл и головная боль, а для сложных доменных процессов, где важна каждая изменение и аудит — иногда единственный адекватный способ не сойти с ума. Выбирай с умом, а то накосячишь — и будешь потом события вручную править, как дурак.