Share
CQRS, Fonctionnel, Event Sourcing & Domain Driven Design — Breizhcamp 2017
Avec autant de buzzwords dans le titre, explicitons le menu :
– Nous commencerons avec une étude des principes du CQRS et la notion de projection pour construire les modèles de données dédiées à la lecture, le tout avec un datastore traditionnel (relationnel). – Nous continuerons avec le concept d’état en programmation fonctionnelle, et comment les gérer au sein d’une application tout en respectant le principe d’immutabilité. Et comment ils ont transformé la gestion d’états pour la construction d’interface utilisateur. – Dans un troisième temps, nous nous intéresserons aux évènements du domaine-métier dans le Domain Driven Design et comment ceux-ci s’intègrent dans la mécanique de construction des projections. – Enfin, nous assemblerons toutes ces notions pour faire apparaitre l’« event sourcing » comme modèle de persistance pour nos données. – Pour clôturer, nous verrons les erreurs les plus courantes rencontrées lors de l’implémentation d’un modèle en event sourcing.
Take away: – Utiliser CQRS (sans event-sourcing) pour simplifier la gestion de la persistance dans son application. – Comprendre comment gérer des états dans un contexte fonctionnel – Gérer facilement les évènements-métier au sein d’une architecture DDD. – Savoir comment implémenter correctement un système basé sur l’event sourcing.
Arnaud LEMAIRE
Transcript
66 slides
-
C Q R S , E V E N T
S O U R C I N G & F U N C T I O N A L P R O G R A M M I N G T H E B U Z Z W O R D C O N F E R E N C E -
@ L I L O B A S E –
W W W. A R P I N U M . F R Arnaud LEMAIRE | Arpinum -
C Q R S C O M M A N
D Q U E RY R E S P O N S I B I L I T Y S E G R E G AT I O N -
C H A N G E S O M E
T H I N G E V E RY T H I N G S TA R T S W I T H U S E C A S E S U S E R I want to … G E T I N F O A B O U T S O M E T H I N G (read) (mutation) C O M M A N D Q U E RY « I need information to make a decision » « I made a decision » -
C O M M A N D S & Q
U E R I E S A R E D T O class RelocateHubCommand implements Command<Void> { public RelocateHubCommand( String hubId, String newLocationId ) { this.hubId = hubId; this.newLocationId = newLocationId; } @NotEmpty public String hubId; @NotEmpty public String newLocationId; } A command returns no business data to the client (can return a status & an id) -
S E N T I N A B U S
@RestController class HubResource { @Autowired public HubResource(CommandBus commandBus) { this.commandBus = commandBus; } @PutMapping("/api/hub/location") public Void relocate(Request request) { Map<String, String> requestBody = Serializer.deserialize(request.body()); return this.commandBus.dispatch( new RelocateHubCommand( requestBody.get("hubId"), requestBody.get("newLocationId") )); } } -
S E N T I N A B U S
@RestController class HubResource { @Autowired public HubResource(CommandBus commandBus) { this.commandBus = commandBus; } @PutMapping("/api/hub/location") public Void relocate(Request request) { Map<String, String> requestBody = Serializer.deserialize(request.body()); return this.commandBus.dispatch( new RelocateHubCommand( requestBody.get("hubId"), requestBody.get("newLocationId") )); } } Almost no stickiness to a framework -
D I S PAT C H E D T O
A S P E C I F I C H A N D L E R @Component public class CommandBus { @Autowired public CommandBus(List<? extends Handler> handlers) { this.handlers = handlers.toMap( h -> Tuple.of(h.listenTo(), h) ); } public <R, C extends Command<R>> R dispatch(C command) { return handlers .get(command.getClass()) .handle(command); } private final Map<Class, Handler> handlers; } -
D I S PAT C H E D T O
A S P E C I F I C H A N D L E R @Component public class CommandBus { @Autowired public CommandBus(List<? extends Handler> handlers) { this.handlers = handlers.toMap( h -> Tuple.of(h.listenTo(), h) ); } public <R, C extends Command<R>> R dispatch(C command<R>) { return handlers .get(command.getClass()) .handle(command); } private final Map<Class, Handler> handlers; } -
T H E F U L L P I P
E L I N E U S E R C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R one per query one per command Q U E RY B U S C O M M A N D B U S -
M I D D L E WA R E @Component
public class BusValidationMiddleware<T> implements BusMiddleware<T, Message<T>> { @Autowired public BusValidationMiddleware( BusMiddleware next, Validator validator ) { this.next = next; this.validator = validator; } public T handle(Message<T> message) { Violation violations = validator.validate(message); if(violations.size() > 1) throw new ArgumentException(violations.toArray()); return next.handle(message); } } -
C O M P O S E D I N
T H E B U S U S E R C O M M A N D Q U E RY new BusValidationMiddleware( new BusTransactionMiddleware( new BusDispatcher( Q U E RY H A N D L E R C O M M A N D H A N D L E R Q U E RY B U S C O M M A N D B U S VA L I D AT I O N C A C H E VA L I D AT I O N U N I T O F W O R K -
C Q R S T H E C O M
M A N D -
C O M M A N D H A N
D L I N G @Component public class RelocateHubCommandHandler implements CommandHandler<Void, RelocateHubCommand> { @Autowired public RelocateHubCommandHandler(HubRepository repo) { this.repository = repo; } @Override public Void handle(RelocateHubCommand command) { Hub hub = repository.getSiteInformation(); hub.relocateTo(command.newLocationId); } public Class listenTo() { return RelocateHubCommand.class; } } -
P E R S I S T E N C
E A B S T R A C T I O N public interface Repository<TId, TRoot> { TRoot get(TId id); void add(TRoot root); void delete(TRoot root); List<TRoot> getAll(); } The persistence mechanism save snapshot of the application state -
P E R S I S T E N C
E A B S T R A C T I O N public class HubRepository implements Repository<UUID, Hub> { public HubRepository(Store store) this.store = store; public Hub get(UUID uuid) return store.findById(uuid); public void add(Hub hub) store.upsert(hub); public void delete(Hub hub) store.remove(hub); public Seq<Hub> getAll() return store.findAll(); } We fetch and save aggregate as a whole -
C O M M A N D H A N
D L I N G @Component public class RelocateHubCommandHandler implements CommandHandler<Void, RelocateHubCommand> { @Autowired public RelocateHubCommandHandler(HubRepository repo) { this.repository = repo; } @Override public Void handle(RelocateHubCommand command) { Hub hub = repository.get(command.hubId); hub.relocateTo(command.newLocationId); } public Class listenTo() { return RelocateHubCommand.class; } } -
H A N D L E R S W O
R K O N A G G R E G AT E public class Hub { public Hub(UUID id) { this.id = id; } public Hub() { this.id = UUID.randomUUID(); } public UUID id() { return id; } //... public void relocateTo(String newLocationId) { locationId = UUID.fromString(newLocationId); } //... private UUID locationId; private UUID id; } We keep under control the aggregate id creation -
H A N D L E R I N T
E R N A L S S T O R E D B A G G R E G AT E S T O R E D B fetched flushed by the unit of work middleware domain operations handler(state, command) = state’ saved -
T E S T I N G @Test public void
it_relocates_a_hub() { store = new InMemoryStore(); command = new RelocateHubCommand("hubId", "newLocationId"); handler = new RelocateHubCommandHander( new HubRepository(store) ); handler.handle(command); expectedHub = store.get("hubId"); assertThat(expectedHub.location()).equals("newLocationId"); } -
C O M M A N D S & Q
U E R I E S A R E D T O class FindHubByLocationPathQuery implements Query<Void> { public FindHubByLocationPath( String locationPath, ) { this.locationPath = locationPath; } @NotEmpty public String locationPath; } -
Q U E RY H A N D L I
N G @Component public class FindHubByLocationPathQueryHander extends SqlHandler<Hub, FindHubByLocationPathQuery> { @Autowired public FindHubByLocationPathQueryHander(SqlConnection sql) { this.sql = sql; } @Override public Installation handle(FetchAllLocationsQuery query) { Record hub = sql.prepare( "SELECT * FROM hub JOIN location ON hub.locationId = location.id WHERE location.path = %s" ).execute(query.locationPath); return Serializer.deserialize(hub); } } We receive a direct connection to the datastore -
Q U E RY H A N D L I
N G @Component public class FindHubByLocationPathQueryHander extends SqlHandler<Hub, FindHubByLocationPathQuery> { @Autowired public FindHubByLocationPathQueryHander(SqlConnection sql) { this.sql = sql; } @Override public Installation handle(FetchAllLocationsQuery query) { Record hub = sql.prepare( "SELECT * FROM hub JOIN location ON hub.locationId = location.id WHERE location.path = %s" ).execute(query.locationPath); return Serializer.deserialize(hub); } } -
C Q R S W R A P P I
N G U P -
T H E S A M E D ATA S
T O R E C A N B E S H A R E D U S E R C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R P E R S I S T E N C E while maintaining a complete segregation through the whole application Q U E RY B U S C O M M A N D B U S -
TA K E A WAY • Massive simplification of persistence
mechanisms • Unit testing in a breeze • Enable business use case discovery • Drive task based UI instead of CRUD thinking -
D O M A I N E V E N
T S T I L L N O T E V E N T S O U R C I N G -
E V E N T ≠ C O M M
A N D C O M M A N D E V E N T Intent : « do something » Event : « something happened » -
E V E N T A R E A L
S O D T O class HubMovedEvent implements Event { public HubMovedEvent( String hubId, String oldLocationId, String newLocationId ) { this.hubId = hubId; this.oldLocationId = oldLocationId; this.newLocationId = newLocationId; } public String hubId; public String newLocationId; public String oldLocationId; } -
A N D A L S O S E N
T I N A B U S eventBus.dispatch( new HubMoved( "hubId", "oldLocationId", "newLocationId" )); -
B U T W I T H M U LT
I P L E H A N D L E R public EventBus(List<? extends EventHandler> handlers) { this.handlers = handlers.toMap( h -> Tuple.of(h.listenTo(), h) ); } public <C extends Event> Void dispatch(C event) { handlers.filter(t -> t._1 == event.getClass()) .forEach(h -> h.handle(event)); } -
A N E V E N T I S T
H E R E S U LT O F A B U S I N E S S O P E R AT I O N public class Hub { public Event relocateTo(String newLocationId) { HubMovedEvent event = new HubMovedEvent( this.locationId.toString(), newLocationId ); this.locationId = UUID.fromString(newLocationId); return event; } UUID locationId; private UUID id; } -
R E T U R N E D B Y
C O M M A N D H A N D L E R S @Component public class RelocateHubCommandHandler implements CommandHandler<Void, RelocateHubCommand> { @Autowired public RelocateHubCommandHandler(HubRepository repo) { this.repository = repo; } @Override public Void handle(RelocateHubCommand command) { Hub hub = repository.get(command.hubId); hub.relocateTo(command.newLocationId); } public Class listenTo() { return RelocateHubCommand.class; } } -
R E T U R N E D B Y
C O M M A N D H A N D L E R S @Component public class RelocateHubCommandHandler implements CommandHandler<Void, RelocateHubCommand> { @Autowired public RelocateHubCommandHandler(HubRepository repo) { this.repository = repo; } @Override public Tuple2<Void, List<Event> handle(RHC command) { Hub hub = repository.get(command.hubId); return Tuple.of( void, List.of(hub.relocateTo(command.newLocationId) ); } public Class listenTo() { return RelocateHubCommand.class; } } -
D I S PAT C H E D B Y
T H E C O M M A N D B U S @Component public class CommandBus { @Autowired public CommandBus(List<? extends Handler> handlers) { this.handlers = handlers.toMap( h -> Tuple.of(h.listenTo(), h) ); } public <R, C extends Command<R>> R dispatch(C command<R>) { return handlers .get(command.getClass()) .handle(command); } private final Map<Class, Handler> handlers; } -
D I S PAT C H E D B Y
T H E C O M M A N D B U S @Component public class CommandBus { @Autowired public CommandBus( Collection<? extends CommandHandler> handlers, EventBus eventBus) { //… this.eventBus = eventBus; } public <R, C extends Command<R>> Try<R> dispatch(C command) { return handlers.get(command.getClass()) .map(h -> execute(h, command)) .getOrElse(() -> Try.failure( new HandlerNotFoundException(command))); } private <R, C extends Command<R>> Try<R> execute( CommandHandler<R, C> h, C command) { return Try.of(() -> { Tuple2<R, List<Event>> result = h.handle(command); result._2.forEach(eventBus::publish); return result._1; }); } } -
D O M A I N E V E N
T P R O J E C T I O N -
L E T ’ S G O B A C
K I N T H E C O M M A N D H A N D L E R U S E R C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R Q U E RY B U S C O M M A N D B U S we are here… -
O U R C O M M A N D
H A N D L E R A G G R E G AT E E V E N T S A G G R E G AT E domain operations handler(state, command) = (state’, [event]) domain event -
A N D R E T U R N S
T H E M public List<Event> handle(RelocateHubCommand command) { Hub hub = repository.getHub(command.hubId); events.add(hub.relocateTo(command.newLocationId)); return events; } private List<Event> events = List.empty(); -
T O B E D I S PAT C H
E D B Y A M I D D L E WA R E C O M M A N D C O M M A N D H A N D L E R C O M M A N D B U S VA L I D AT I O N U N I T O F W O R K E V E N T D I S PAT C H E R S E N D E M A I L L A U N C H C O M M A N D -
W E C A N L I S T E
N E V E N T T O U P D AT E O T H E R D ATA S T O R E public class HubLocationsUpdater implements EventHandler<HubMovedEvent> { public HubLocationsUpdater(Connection sql) { this.sql = sql; } public void handle(HubMovedEvent event) { sql.createStatement().executeUpdate( "UPDATE hub_location SET location_id = ? WHERE hub_id = ?" ).execute(event.newLocationId, event.hubId); } public Class<? extends Event> listenTo() { return HubMovedEvent.class; } } -
W E C A N C R E AT E
A R E A D M O D E L I N T H E S A M E D ATA S T O R E C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R P R O J E C T I O N U P D AT E R E V E N T D I S PAT C H E R P E R S I S T E N C E W R I T E M O D E L R E A D M O D E L -
O R I N D I F F E R
E N T O N E C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R P R O J E C T I O N U P D AT E R E V E N T D I S PAT C H E R W R I T E M O D E L R E A D M O D E L This is not event sourcing ! -
O P E N I N G A W O
R L D O F P O S S I B L E G E O Q U E RY S E A R C H U P D AT E R D O M A I N E V E N T G I S D B G I S U P D AT E R S Q L U P D AT E R E L A S T I C S E A R C H P O S T G R E S Q L S E A R C H Q U E RY R E L AT I O N A L Q U E RY You can adapt your datastore to your queries need plus you can even add more later… -
TA K E A WAY • Enable reactive business system
• Adapt the datastore to your queries need • Suited even for legacy system -
E V E N T S O U R C
I N G AT L E A S T … -
W E A D D S O M E M
E TA D ATA T O E V E N T S public abstract class Event<ID, ENTITY extends AggregateRoot<ID>> { public Event(ENTITY entity) { aggregateId = entity.id(); aggregateType = entity.class; } public ID aggregateId; public Class<ENTITY> aggregateType; public LocalDateTime eventDateTime = LocalDateTime.now(); } -
W E A D D S O M E M
E TA D ATA T O E V E N T S public class HubMovedEvent extends Event<UUID, Hub<UUID>> { public HubMovedEvent( Hub hub, String oldLocationId, String newLocationId ) { super(hub); this.hubId = hub.id().toString(); this.oldLocationId = oldLocationId; this.newLocationId = newLocationId; } public String hubId; public String newLocationId; public String oldLocationId; } -
D O M A I N E V E N
T A S S O U R C E O F S TAT E public class HubEventApplier implements EventApplier<Hub> { public static Hub apply(Hub root, HubMovedEvent event) { Hub hub = new Hub(root); hub.locationId = UUID.fromString(event.newLocationId); return hub; } } look like a reducer, no ? -
F R O M B U S I N E
S S O P E R AT I O N A S M U TAT I O N public Event relocateTo(String newLocationId) { HubMovedEvent event = new HubMovedEvent( this, this.locationId.toString(), newLocationId ); this.locationId = UUID.fromString(newLocationId); return event; } -
T O E V E N T S O U
R C E D B U S I N E S S O P E R AT I O N S public Tuple2<Hub, Event> relocateTo(String newLocationId) { HubMovedEvent event = new HubMovedEvent( this, this.locationId.toString(), newLocationId ); return Tuple.of( HubEventApplier.apply(this, event), event ); } -
R E A L E V E N T A
P P L I E R public class HubEventApplier implements EventApplier<Hub> { static Hub applyHubMovedEvent(Hub h, HubMovedEvent event) { Hub hub = new Hub(h); hub.locationId = UUID.fromString(event.newLocationId); return hub; } public static Hub apply(Hub hub, Event event) { return appliers .get(event.getClass()) .apply(hub, event); } static Map<Class, Function> appliers = HashMap.of( HubMovedEvent.class, (hub, event) -> applyHubMovedEvent(hub, event) ); } -
A N D S AV E D I N E
V E N T S T O R E C O M M A N D C O M M A N D H A N D L E R E V E N T S T O R E this is event sourcing E V E N T P E R S I S T E N C E C O M M A N D C O M M A N D H A N D L E R D B U N I T O F W O R K flush serialize -
W H AT I S A N E V E
N T S T O R E ? -
W H AT I S A N E V E
N T S T O R E ? -
E V E N T S O U R C
I N G R E B U I L D I N G S TAT E -
W E G E T E V E N T
S F O R A G I V E N A G G R E G AT E public class eventStore { public eventStore(Connection sql) { this.sql = sql; } public List<Event> get(String type, String id) { Seq<Record> records = sql.createStatement( " SELECT * FROM events WHERE aggr_type = ? AND aggr_id = ? ORDER BY TIMESTAMP " ).execute(aggregateType, aggregateId); return records.map(records -> Serializer.deserialize( record.getValue("payload"), Event.class )); } } -
A N D W E A P P LY T
H E M public HubRepository(EventStore store) { this.store = store; } public Hub get(UUID uuid) { List<Event> events = store.get("HUB", uuid.toString()); return events.foldLeft( new Hub(), (hub, event) -> HubEventApplier.apply(hub, event) ); } -
TA K E A WAY • No more business memory
loss • Pure data persistence (no more plop) • You can build new projection from past events -
F O L D E R H I E R
A R C H Y -
H O W W E H A N D L
E C O M M A N D • classic : handler(state, command) = state’ • with domain event : handler(state, command = (state’, [event]) • with event sourcing : handler([event], command) = [event]’ -
Q & A • How do we snapshot ? •
How do we manage version ? • How to use multiple aggregate in a handler ? -
T O G O F U R T H E
R • CQRS Documents by Greg Young • The Value of Value by Rich Hickey • What if the user was a function by Andre Staltz -
@ L I L O B A S E |
A R P I N U M . F R Thanks !