Review PersistentEntity API for reply and afterPersist
See original GitHub issueThis issue is a starting point for a broader discussion we would like to initiate about the current state of PersistentEntity
API.
This was initially triggered by PR #901. The main problem reported there is that it’s very cumbersome to write command handlers that reply with the entity state while emitting many or none events.
For illustration: (examples in Java syntax)
final List<Event> createdEvents = generateEvents();
if (createdEvents.size() == 0) {
ctx.reply(state().getModel());
return ctx.done();
} else {
return ctx.thenPersistAll(
createdEvents,
() -> { ctx.reply(state().getModel()); }
);
}
In its current form, the API offers no easy way to deal with the fact that events may not be emitted.
The ctx.thenPersistAll
method receive an empty lists of events, but the afterPersist
method is not called if there are zero events persisted. Which make sense, because it’s called afterPersist
.
The mechanism is based on aContext
that is passed to the user. The Context
brings to user space (without exposing it) the actor.sender()
. In the case of reply
, behind the scene the reply
calls sender() ! someMessage
.
Note that we are basically piggybacking the afterPersist
method to call indirectly the actor.sender()
method.
Fluent API Proposal
We could achieve exactly the same without relying on a context and without mixing side-effect function (afterPersist
) with reply
functions.
For instance…
PersistAll(generateEvents())
.replyWith( state -> state.getVersion(); );
The above example replies by returning the version
of model. The state
is the model after applying the generated events. If generatedEvents
return a empty list, state
is not updated.
Note that this API removes completely the need for a Context
. We just register a function State -> A
that is called to transform the current state and generate the data that will be sent back to the sender.
Another variation could be:
PersistAll(generateEvents())
.replyWith( (state, events) -> state.getVersion(); );
In that case, the user receives the current updated state
and the generated events.
A similar fluent API can be implement for afterPersist
.
PersistAll(generateEvents())
.afterPersist( events -> // do some side-effect here ) // <- symmetric api, see below
.replyWith( state -> state.getVersion(); );
afterPersist
callback is only called if events.nonEmpty
and it returns void
or Unit
(scala).
PersistOne / PersistAll asymmetry
The afterPersist
callbacks for PersistOne
and PersistAll
are not symmetric.
PersistOne
has Event -> void
and PersistAll
has () -> void
.
The reason for that comes from akka-persistence itself where the handler
for persistAll
has the same shape (Event -> void
) as the handler for persist
. This is probably for historical reasons, but as a consequence the Lagom API exposed it as Event -> void
and () -> void
.
Some users have complained on Gitter about it. There is a easy workaround which is to have the generated events in scope when defining the command handler. However, we can easily improve the user experience by offering a symmetric API.
Issue Analytics
- State:
- Created 6 years ago
- Reactions:4
- Comments:16 (15 by maintainers)
Top GitHub Comments
As a first observation,
seems better than
I really like the explicit handling of entity creation. How would deletion be handled? By an event-handler that sets the state to None or an explicit API as well?