Казахстан, Караганда
Software Engineer
фанат практик XP/EngX
Верю в Agile
И люблю Java
а точнее кучу одинаковых проектов
Командую "сидеть"
Прошу "принести палку"
Command and Query
сюда идем за чтением
все операции по изменению
не забываем синхронизировать
Материал.
представление
Основная структура данных
Прило
жение
То масштабируем только там, где надо!
Сервис для записи
Сервис для чтения
для отчета
для графиков
структура для списка
данные по нескольким агрегатам
Первое событие
Второе событие
...
...
...
...
...
Последнее событие
Подать заявку
Первое ревью идеи
...
Получить финальный отзыв
Череда событий
дельта изменений
Все события в начальной очередности
дельта изменений
дельта изменений
дельта изменений
дельта изменений
Удалить event
Изменить event
name: "JPoint 2018",
id: "CONF-785seeuis3",
location: "Moscow",
organizator: "JUG.ru",
status: "PLANNED"
NewConferencePlanned
id: "CONF-785seeuis3",
name: "JPoint 2018",
location: "Moscow",
organizator: "JUG.ru"
name: "JPoint 2018",
id: "CONF-785seeuis3",
location: "Almaty",
organizator: "JUG.ru",
status: "PLANNED"
ConferenceNewLocation
id: "CONF-785seeuis3",
location: "Almaty"
name: "JPoint 2018",
id: "CONF-785seeuis3",
location: "Almaty",
organizator: "JUG.ru",
status: "CANCELED"
ConferenceCanceled
id: "CONF-785seeuis3"
name: "JPoint 2018",
id: "CONF-785seeuis3",
location: "Almaty",
organizator: "JUG.ru",
status: "PLANNED"
ConferencePlannedAgain
id: "CONF-785seeuis3"
сюда идем за чтением
все операции по изменению
не забываем синхронизировать
Реляционная модель
Пишем и читаем с event store
События как источник данных
История — сокровищница наших деяний, свидетельница прошлого, пример и поучение для настоящего, предостережение для будущего.
Но помни, у нас больше полей!
событие/история
событие/история
событие/история
событие/история
событие/история
ID | aggregateId | creationDate | userId | revision | data | type |
---|
{
id: "CONF-785seeuis3",
name: "JPoint 2018",
location: "Moscow",
organizator: "JUG.ru"
}
"CONF-45rs21u"
22/12/1990
"U-412gjk"
1
23
"AddNewConference"
@Getter
@Setter
@ToString
@Entity
@Table(name = "EVENTS")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorValue("base")
@DiscriminatorColumn(name = "type")
@EqualsAndHashCode(of = {"id", "revision"})
@TypeDefs({
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
public class BaseEvent<A extends BaseAggregate> implements Event {
@Id
@Type(type = "pg-uuid")
@Column(name = "EVENT_ID")
private UUID id;
private int revision;
@Column(name = "AGGREGATE_ID")
private String aggregateId;
@Column(name = "CREATION_DATE")
private LocalDateTime creationDate = LocalDateTime.now();
@Type(type = "jsonb")
private String data;
@Column(name = "USER_BUSINESS_ID")
private String userId;
@Transient
private A aggregate;
}
Дельта изменений
Внешний ключ
@Getter
@Setter
@Entity
@Table(name = "AGGREGATES")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorValue("base")
@DiscriminatorColumn(name = "type")
@EqualsAndHashCode(of = {"id", "revision"}, callSuper = true)
public class BaseAggregate extends AbstractFunctionalAggregate {
@Id
@Column(name = "BUSINESS_ID")
private String id;
@JsonIgnore
private int revision;
private String description;
@Column(name = "CREATION_DATE")
private LocalDateTime creationDate = LocalDateTime.now();
@Column(name = "MODIFICATION_DATE")
private LocalDateTime modificationDate = LocalDateTime.now();
}
@Getter
@Setter
@ToString
@Entity
@EqualsAndHashCode(callSuper = true)
@DiscriminatorValue("conference")
public class ConferenceAggregate extends BaseAggregate<BaseEvent> {
@Transient //это поле состояния, будет установлено событием
private String name;
@Transient ///это поле состояния, будет установлено событием
private ConferenceStatus status = ConferenceStatus.PLANNED;
...
}
@Getter
@Setter
@Entity
@DiscriminatorValue(СonferenceCancelEvent.EVENT_TYPE)
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class СonferenceCancelEvent extends BaseEvent {
public static final String EVENT_TYPE = "conference-cancel";
@Override
public void copyTo(Aggregate aggregate) {
ConferenceAggregate conference= (ConferenceAggregate) aggregate;
conference.setStatus(ConferenceStatus.CANCELED);
}
....
}
@Getter
@Setter
@Entity
@DiscriminatorValue(СonferenceRenameEvent.EVENT_TYPE)
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class СonferenceRenameEvent extends BaseEvent {
public static final String EVENT_TYPE = "conference-rename";
@EventFieldData // кастомная анотация, говорит о том, что поле будет в JSON
@Transient //это не колонка в таблице, хранится внутри JSON
private String name;
@Override
public void copyTo(Aggregate aggregate) {
ConferenceAggregate conference= (ConferenceAggregate) aggregate;
conference.setName(name);
}
....
}
В поле data
(Base Event)
Конференция создана
Добавлена структура
Включен доклад
Обновлен доклад
...
{
topic: "Event Sourcing & CQRS",
speaker: "Ануар Нурмаканов",
type: "Введение в технологию",
day: "первый",
sector: "Зал 1",
...
}
{
day: "второй"
}
1
2
3
4
...
...
...
...
...
...
Одна таблица
Другая таблица
...
Зачастую это все ведет к N-му количеству JOIN или нескольким запросам для получения данных
Вот так
Или так
Ну или так
Информация о докладе | Информация о спикере | Ревьюверы | Прошлые выступления |
---|---|---|---|
Новый доклад
+ 1 ревьювер
первый
feedback
Одна таблица
Другая таблица
...
создать/сохранить
Материал.
представление
Основная структура данных
Прило
жение
Таблица с подготовленными данными для отчетов
Готовим данные для выборки и отчетов заблаговременно
SELECT to_tsvector('text')
@@ to_tsquery('query');
Создали график
Открыли доступ
Сделали резервацию
Поменяли резервацию
Приостановили резервацию
Отменили
Сохранил
Иногда отредактировал
Все
Мало событий! Важно финальное состояние!
У нас есть просто Apache Kafka =)
Anton Udovychenko, Java Day Kyiev
переименовать хочу
хочу видеть этот доклад с 3:00 до 4:00
Шикарный доклад с 3:00 до 5:00
AddNewSpeech {
day: "one",
start: "3:00",
end: "4:00",
stream: "1"
}
RenameConference{
newName: "Joker rules!"
}
AddNewSpeech {
day: "one",
start: "3:00",
end: "4:00",
stream: "1"
}
RenameConference{
newName: "Joker rules!"
}
AddNewSpeech {
day: "one",
start: "3:00",
end: "4:00",
stream: "1"
}
AddNewSpeech {
day: "one",
start: "3:00",
end: "5:00",
stream: "1"
}
AddNewSpeech {
day: "one",
start: "3:00",
end: "4:00",
stream: "1"
}
AddNewSpeech {
day: "one",
start: "3:00",
end: "5:00",
stream: "1"
}
не решаемый
конфликт
решаемый
конфликт
В какой-то момент стоит задуматься об производительности
если чтение только с модели для чтения
event лог
модель для чтения
Запись
Чтение
Удалить
Запись еще не удалена
небольшие задержки это вполне нормально в большинстве своем
Инвестируйте в Message Broker
материализованное состояние
событие n-1
событие n
Отправлю-ка я заявку на Joker в Санкт-Петербург
Отправлю-ка я заявку на Joker в Санкт-Петербург
Отправлю-ка я заявку на Joker в Санкт-Петербург
Отправлю-ка я заявку на Joker в Санкт-Петербург
Запомнить
Сохранить заявку
Проверка качества
Заявка принята
Заявка отменена
Сохраняем наш event - заявка принята(даже если она не очень)
Запускаем все нужные проверки
Добавляем event после верификаций
Ништяк
Используйте модель для чтения
В теории сколько угодно
Нам хватило и меньше
История и лог изменений
Event-Driven Architecture
Прокрутить события назад
отсутствие событий как таковых
важно финальное состояние
много сложных запросов
много сложных запросов
неплох при event sourcing
большая нагрузка на чтение
разные модели для чтения
Chris Richardson
Steve Pember
Sebastian Daschner
Martin Fowler
Greg Young