Active Record Projection
Persistent Rails Event Store projections built on Active Record models.
Built on the Rails Event Store Aggregate Root.
How it works:
- a projection is built from a stream of events
- a projection only has a single stream but a stream can be the source of multiple projections
- each projection has a polymorphic association to a record
- this record is your Active Record model that functions as an aggregate root
- it can update its own state and the state of its children by projecting events
- an async event handler is automatically subscribed to all events that you project using the
onmethod- when an event of each type occurs, the projections subscribed to all streams in which that event is linked are notified
- unseen events are then read, applied and the record is saved
- optimistic locking is used to control concurrent updates to the projection/record
---
title: Records and Projections
---
classDiagram
namespace ActiveRecordProjection{
class Projection{
String: stream
Integer: lock_version
UUID: last_event_id
Integer: record_id
String: record_type
}
}
class AggregateRoot{
}
class Child{
}
namespace ActiveRecord{
class Base{
}
}
namespace RailsEventStore{
class Stream{
}
}
AggregateRoot --|> Base
Child --|> Base
AggregateRoot *-- Child
Projection o-- AggregateRoot : record
Projection o-- Stream : stream
Installation
Add to your Gemfile and run bundle.
gem 'active_record_projection'Create a active_record_projection_projections table in your database:
bundle exec rails generate active_record_projection:installAdding an Active Record Projection
To make your Active Record model a projection of an aggregate root:
- include
ActiveRecordProjection - define a
get_streammethod to allow a stream to be determined from the model data - define how to project each event using the Aggregate Root syntax (define
onmethods)
class Order < ApplicationRecord
include ActiveRecordProjection
on OrderSubmitted.event_type do |event|
self.state = :submitted
self.delivery_date = event.data.fetch(:delivery_date)
end
on OrderExpired.event_type do |_event|
self.state = :expired
end
private
def get_stream
"orders$#{uuid}"
end
endYou need to add a on method for each event type in a stream, even if you don't need it in order to project.