Journal
Source<?>
types—DomainEvent
and Command
, for example—into a CQRS query model.ProjectToDescription
is a description of how data and events are filtered and selected. You may configure a ProjectToDescription
for projecting State<T>
and also for projecting Source<T>
instances, such as DomainEvent
types. Both are described below.Dispatcher
and ProjectionDispatcher
to be registered with the storage mechanism is created by means of an actor that implements both protocols. Two such are the BinaryProjectionDispatcherActor
and TextProjectionDispatcherActor
, and used when a binary store or text store is used, respectively.Dispatcher
registered with it.ProjectToDescription
for State<T>
ProjectToDescription
may be defined for state data and for events. The following shows a description defined for a state object.Metadata
, and the Metadata
instance may have a named operation
that caused the state to transition to the current value. The above description indicates that if the state data contains an operation
that matches "User:new"
, "User:contact"
, or "User:name"
, the dispatcher will route the data to the UserProjectionActor
.operation
name is associated with the new state. In this case the command is to replace the user's name with a different name. The transitioned state and the descriptive operation is applied. After the state is persisted, the Dispatcher
is given the opportunity to route the new state to a projection. As seen in the description of the previous code snippet, matching the "User:name"
operation will cause the new state to be routed to the UserProjectionActor
.ProjectToDescription
for Source<T>
ProjectToDescription
may be defined for state data and for events. The following shows a description defined for events.DomainEvent
is one of the types ForumStarted
, ForumClosed
, or ForumModeratorAssigned
, the dispatcher will route the event to the ForumProjectionActor
.ForumStarted
event occurs. After the event is persisted, the Dispatcher
is given the opportunity to route it to a projection. As seen in the description of the previous code snippet, matching the ForumStarted
event will cause it to be routed to the ForumProjectionActor
.ProjectToDescription
up to date with new and changing event types could be error prone by being overlooked. Instead of referencing each event type, you may instead reference the Java package that contains all of the events.Dispatcher
and ProjectionDispatcher
ProjectToDescription
, Dispatcher
, ProjectionDispatcher
, TextProjectionDispatcherActor
, and Journal
are wired for use.ProjectionDispatcherProvider
example.ProjectToDescription
instances created, which are formed into a single List<ProjectToDescription>
. Each of the descriptions indicate how various command model changes will be dispatched into the actor instance whose type is the first parameter. For example, this DashboardProjectionActor
instance will be created inside the ProjectionDispatcher
and all DomainEvent
types defined as inner classes of the com.dashdawn.model.dashboard.Events
class, will be dispatched to it. The same goes for the WaveBoardDetailsProjectionActor
and the inner Events
types defined in its package.descriptions
are then used to create the ProjectionDispatcher
, which is implemented by the TextProjectionDispatcherActor
provided by XOOM LATTICE. Actually this actor implements two protocols, both the Dispatcher
and the ProjectionDispatcher
. The Dispatcher
is used by the Journal
to dispatch newly appended events to the ProjectionDispatcher
. (This is implemented by the abstract base ProjectionDispatcherActor
, which is extended by the concrete actors, TextProjectionDispatcherActor
and BinaryProjectionDispatcherActor
). The two protocol references answered by actorFor()
are captured in the Protocols
type.ProjectionDispatcherProvider
instance is created, where both protocol references are held and available to dependents.Dispatcher
is used, where the command model storage will use it to dispatch events into the projections. Note that the command model store knows nothing about projections, only the Dispatcher
.Bootstrap
or Startup
class containing a main()
.Projection
InterfaceProjection
designed to handle state mutation operations. It corresponds to the above section ProjectToDescription
for State<T>
.Projectable
contains the data to be projected. The ProjectionControl
is used to confirm that the Projectable
has been projected so that the Dispatcher
will not route it again. The code inside each of the cases would create or update the views effected by the state mutations.Projection
designed to handle events. This example corresponds to the above section ProjectToDescription
for Source<T>
.StateStoreProjectionActor
Actor
abstract base type for this: StateStoreProjectionActor
. This is a much more powerful abstract than directly using the Projection
interface. As the name indicates, this projection base is used to project into a StateStore
.StateStoreProjectionActor
.merge(...)
methods should be overridden.protected T merge(
final T previousData,
final int previousVersion,
final T currentData,
final int currentVersion)
protected T merge(
final T previousData,
final int previousVersion,
final T currentData,
final int currentVersion,
final List> sources)
StateStoreProjectionActor
, as demonstrated next. This example overrides the merge(...)
that is best used with Event Sourcing.DashboardProjectionActor
constructor uses the super constructor that takes only the StateStore
used to write and read DashboardView
instances. This single-parameter constructor provides a default StateAdapter
and EntryAdapter
for the concrete projection to use. These are part of XOOM SYMBIO and are known as DefaultTextStateAdapter
and DefaultTextEntryAdapter
.StateStoreProjectionActor
that takes an additional two parameters, one for the StateAdapter
and one for the EntryAdapter
. State
and Entry
types to the model types. When the single argument constructor is used you get the default StateAdapter
and EntryAdapter
. EntryAdapter
supported, but no doubt several or many DomainEvent
types needed, your own custom EntryAdapter
must internally hold the full number of EntryAdapter
instances needed for all DomainEvent
types. This might use a Map
with keys of Class<? extends DomainEvent>
and values of concrete EntryAdapter
instances. The outer EntryAdapter
registered with the StateStoreProjectionActor
would operate by looking up the specific concrete EntryAdapter
by DomainEvent
type, dispatch to it, and answer its return value.DashboardProjectionActor
implements the Projection
protocol, but there is a default implementation of method projectWith()
provided by the abstract base classStateStoreProjectionActor
. The implementation of this method is quite simple for most uses. If you need a more involved implementation you may override the default provided by the abstract base class.Projectable
, which may hold a state, or entries, or both.upsertFor()
does the following.currentDataFor(projectable)
for its notion of the current state, as in the state that is now being projected. You may override currentDataFor(projectable)
. This may or may not be useful, but if it is, you must return the value that is considered current from the Projectable
. This method is generally used only for full-state projections rather than those based on DomainEvent
instances.prepareForMergeWith()
. The default behavior adapts any entries()
in the Projectable
to Source<?>
instances, holding them in an internal List<Source<?>>
. The concrete extender may get the adapted Source<?>
instances using the sources()
method. These are provided by default with the second merge(...)
method that is used for Event Sourcing projections. If you have some alternative or additional preparation to do before the merge this method must be overridden.T
value from the StateStore
(see the last statement in the method) and passes a BiConsumer<T, Integer> upserter
. When the read completes, the upserter
is used to merge the previousData
with the currentData
and/or apply any Source<?>
instances.StateStore
if alwaysWrite()
is true
or if the newly merged data is not equal to the previous data. This happens inside the upserter
.DashboardProjectionActor
implements currentDataFor()
as follows.currentDataFor()
may or may not be useful in terms of providing a new state. This implementation answers an Empty
instance of the DashboardView
, which will be used by the merge()
as an accumulator of modifications. This particular override example is unnecessary but demonstrates an alternative to the default implementation. Assume that we decide not to implement the above override and remove the Empty
value declaration as well as the currentDataFor()
method override.currentDataFor()
and merge()
similar to the previous section is necessary.DashboardProjectionActor
is given the opportunity to prepare for the merge.prepareForMergeWith()
adapts all Entry
instances into instances of the DomainEvent
specialization IdentifiedDomainEvent
. This gives the concrete event types the means to provide the identity of the event instance. As indicated above, this specific implementation of prepareForMergeWith()
is redundant because it does the same that the default behavior already does. You would choose to override prepareForMergeWith()
only if you required behavior beyond the above implementation.prepareForMergeWith()
the upsert()
calls dataIdFor()
.dataId
is provided by the first (and possibly only) instance of the IdentifiedDomainEvent
in the events
list. Recall that this events
list was just previously populated by the prepareForMergeWith()
. This example dataIdFor()
method override is likely redundant because the default behavior provides a more thorough implementation, first checking for the Projectable dataId()
, and if that is not available, looks for the identity from one of the first IdentifiedDomainEvent
. If neither of these are available then the dataId
will be a blank String
, and an override of this method would be necessary to provide a custom value.prepareForMergeWith()
, the source/event instances are retrieved as follows. Here again, your DomainEvent
instances must be of the extends IdentifiedDomainEvent
type, which can provide the identity()
of the entity that emitted them.DashboardProjectionActor
implementation of merge()
and its helper method mergeInto()
, assuming that the prepareForMergeWith()
was also overridden and a local List<DomainEvent>
of events
was collected by it. This example shows an alternative to using the more appropriate merge(...)
override that provides the List<Source<?>>
parameter.merge()
passes the currentData
to be used by mergeInto()
as an accumulator of the new state. Any preexisting state on the currentData
that coincides with the interpretation of the DomainEvent
will be overwritten/replaced.Projectable
may have multiple entries (serialized DomainEvent
instances), the mergeInto()
iterates over the entire collection of events deserialized by the method prepareForMergeWith()
.prepareForMergeWith()
implementation, you would iterate as follows, using the sources()
method rather than a collection that you gathered on your own.switch
uses an enum
of DashboardViewProjectableType
to match on each of the DomainEvent
instances.DomainEvent
type is matched to the enum by the match()
method. A non-match produces the Unmatched
type, which is ignored by mergeInto()
, but logged as a warning.mergeInto()
maps event attributes to DashboardView
attributes. Any preexisting values that are mapped are overwritten on the view
accumulator. The iteration and matching is done for any number of events, which may be included with the current Projectable
or others received in the future. DashboardView
to directly use public mutable attributes was done to keep the implementation simple, albeit somewhat "dangerous" according to opinions. It doesn't seem like a bad thing in this specific case because the view object is used in very specific limited ways. Feel free to include accessor methods for reading and writing in your designs.merge()
completes, the modified DashboardView
is written to the StateStore
.StateStoreProjectionActor<T>
provides a default means to determine the current version of the data to be merged and then updated to the database. It works like this:Projectable
is used as the version for the current data to be merged and updated to the database. This is how all of the above examples obtain the current data version for inserts/updates. It is based on a policy to always update the current merged data. That is, the data is always written no matter whether it is the same as the previous data, or different.Projectable
is redelivered due to a latent or failed delivery confirmation. In such cases the StateStoreProjectionActor<T>
provides a secondary policy, which is to automatically increment the previous version by one and to also test the current merged data with the previous data to qualify the need to update the data to the database.alwaysWrite()
method. By default it returns true
. Your override should return false
instead.currentDataVersionFor()
, which you need not override if the secondary policy works for your specific projection use case.currentDataVersionFor()
provides both policies in one line of code. If alwaysWrite()
answers true
, the projectable.dataVersion()
is returned as was explained above. Otherwise, when alwaysWrite()
answers false
, the next sequential version from the one previously stored in the databased is returned. Note that in the case where there was no previous data in the database for the given identity, the previousVersion
is -1
. Thus, there is the one special case where 1
must be explicitly returned as the initial version.ProductDefined
from one Product
entity will have version 1
, and ProductDefined
from a different Product
entity will also have version 1
. This is unavoidable because ProductDefined
is always the first event emitted from a Product
. If you attempt to merge the ProductDefined
that is delivered after the first initially delivered ProductDefined
, the merged/updated data will have version 1
just as the previous data did. Writing this version will cause an optimistic concurrency violation in the StateStore
, because the version will make the new data look as if it is based on stale state. By overriding alwaysWrite()
to answer false
, your current/merged data version will always be one greater than the previous data version.alwaysWrite()
answers false
, there will be a pre-update check requiring the current data to be different from the previous data.