Projector
Projectors are responsible for creating projections based on events. Projections are records in tables.
Sequent uses ActiveRecord
for CRUD-ing records in the database. Sequent uses the term Records
to
describe the Projections. In Sequent Projectors inherit from Sequent::Projector
. To store something
in a Projection you need 3 things in Sequent
:
- A Projector. Responsible for creating Projections. We are discussing Projectors in this chapter.
- A Record class. This is a
Sequent::ApplicationRecord
class, subclassingActiveRecord::Base
. In Sequent Records can only be updated/created/deleted inside Projectors. The rest of the application needs to regard these objects as read-only. This however is not enforced in Sequent. - A SQL file. The SQL contains the table definition in which the Record will be stored. Please checkout the chapter on Migrations for an in-depth description on how migrations work in Sequent.
The nature of view state in event sourced applications is not compatible with ActiveRecord
migration
model. Therefor we don’t use it. In event sourced applications the view state is always derived
from Events. So when you want to have another view state, (maybe you add a column, or group some attributes)
you replay the affected Projectors.
In Sequent you define a Projector as follows:
class UserProjector < Sequent::Projector
manages_tables UserRecord
end
Sequent::Projector
exposes the manages_tables
method in which you state which
Records this Projector manages. There are two important things you need to know:
- A Record can only be managed by one Projector. A Projector can however manage multiple Records.
- A Projector should only access Records it manages.
A Projector is used in two different stages in your application.
- During normal operation. This is when your application is running an Events are coming in. The Projector updates the Records as you specified.
- During migrations. During a migration some Projectors are rebuild in the background to build up new projections. Because of this a Projector can only access Records it manages, since the other Projector might not be finished yet rebuilding. In Sequent we replay on a per aggregate basis.
To use Projectors in your project you need to add them to your Sequent configuration:
Sequent.configure do |config|
config.event_handlers = [
UserProjector.new
]
end
Creating a Record
To create a Record in Sequent you add a code block that listens to the appropriate Event and creates a Record in the database.
class UserProjector < Sequent::Projector
on UserCreated do |event|
create_record(UserRecord, {aggregate_id: event.aggregate_id})
end
end
Internally a Projector uses a Sequent::Core::Persistors::Persistor
to access the database.
During normal operations this is the ActiveRecordPersistor
. This means the above code
is eventually translated to:
user_record = UserRecord.new(aggregate_id: event.aggregate_id)
user_record.save!
Sequent::Projector
provides a set of methods to create/read/update/delete Records as wrapper
around ActiveRecord
. Reason for the extra abstraction is performance during migration of Projectors.
During a migration a highly optimized Persistor, the ReplayOptimizedPostgresPersistor
is used to speed up bulk inserting.
Because of the abstraction you need to use the provided wrapper methods.
This poses some restrictions on how you can use ActiveRecord
functionality.
For instance you can not add child
relations via the parent as you might be used to do in a “normal” ActiveRecord application.
parent = ParentRecord.new
parent << ChildRecord.new
parent.save!
In Sequent this will not work. You need to persist child records the same as you would persist the parent record.
class UserProjector < Sequent::Projector
on ParentCreated do |event|
create_record(ParentRecord, {aggregate_id: event.aggregate_id})
event.children.each do |child|
create_record(ChildRecord, {parent_record_id: event.aggregate_id, child_id: child.child_id})
end
end
end
Updating a record
To update a Record in Sequent use the update_all_records
method. This method has 3 parameters:
- the Record
- the where clause as a
Hash
- the updates as a
Hash
class UserProjector < Sequent::Projector
on UserNameSet do |event|
update_all_records(
UserRecord,
event.attributes.slice(:aggregate_id), # the where clause as a hash
event.attributes.slice(:firstname, :lastname) # the updates as a hash
)
end
end
Tip: You can access all attrs
from an Event via the attributes
method. This returns a Hash
on
which you can call slice
which returns a Hash
containing the key value pairs of the
keys you requested. This is extra handy of the name in the attrs
are the same as the column
names in your table definition.
Deleting a record
Deleting a Record is pretty straight forward. Call the delete_all_records
with the Record and where clause.
class UserProjector < Sequent::Projector
on UserDeleted do |event|
delete_all_records(
UserRecord,
event.attributes.slice(:aggregate_id), # the where clause as a hash
)
end
end
Reading a record
You can also read a Records in a Projector. This is not very common but handy from time to time. For instance you could update the search column for a record for easy searching.
class UserProjector < Sequent::Projector
on UserNameSet do |event|
user_record = get_record!(UserRecord, event.attributes.slice(:aggregate_id)
search_field = "#{user_record.search_field} #{event.firstname} #{event.lastname}"
update_all_records(
UserRecord,
event.attributes.slice(:aggregate_id), # the where clause as a hash
event.attributes.slice(:firstname, :lastname).merge(search_field: search_field) # the updates as a hash
)
end
end