Event Sourced Record offers an idiomatic way to use the Event Sourcing pattern in Rails code.
With Event Sourcing, every change to the state of an object is recorded as an immutable event in a replayable sequence. The result is decoupled code that simplifies state debugging and retrospective reporting.
For more, see Martin Fowler's writeup of the pattern: http://martinfowler.com/eaaDev/EventSourcing.html
Event Sourced Record supports Rails 3.2 and higher.
Add this line to your application's Gemfile:
gem 'event_sourced_record'
And then execute:
$ bundle
Or install it yourself as:
$ gem install event_sourced_record
Event Sourced Record uses observers, so you'll need to add them to your Gemfile:
gem 'rails-observers'
See Getting_Started.md
for a detailed example.
Generate the required classes with rails generate event_sourced_record
:
rails generate event_sourced_record Subscription \
user_id:integer bottles_per_shipment:integer \
bottles_left:integer
The argument list is the same as with rails generate model
. This will
generate the event model, the projection, the calculator, the observer,
and a Rake file.
The event model is an ActiveRecord model but it can act significantly different based on what type of event it is. Use event_type
to configure these types:
class SubscriptionEvent < ActiveRecord::Base
include EventSourcedRecord::Event
serialize :data
belongs_to :subscription,
foreign_key: 'subscription_uuid', primary_key: 'uuid'
event_type :creation do
attributes :bottles_per_shipment, :bottles_purchased, :user_id
validates :bottles_per_shipment, presence: true, numericality: true
validates :bottles_purchased, presence: true, numericality: true
validates :user_id, presence: true
end
event_type :change_settings do
attributes :bottles_per_shipment
validates :bottles_per_shipment, numericality: true
end
end
If you are using mass-assignment protection, which is on by default in Rails 3.2, you may want to make these attributes mass-assignable with
attr_accessible
.
The easiest way to create these records is with the scopes that are automatically generated by event_type
:
SubscriptionEvent.creation.create!(
bottles_per_shipment: 1, bottles_purchased: 6, user_id: current_user.id
)
The event attributes are stored in the data
column, which by default is
serialized to a text field. If you'd prefer to use e.g. JSONB with PostgreSQL
9.4+, just remove the call to serialize
and modify the migration to use your
preferred type.
The projection is the ActiveRecord model that is generated deterministically with the data in the timestamped events combined with the logic in the calculator. Projections shouldn't have any code for modifying themselves, as that will be done externally. Accordingly, projections end up being fairly small classes:
class Subscription < ActiveRecord::Base
has_many :events,
class_name: 'SubscriptionEvent',
foreign_key: 'subscription_uuid',
primary_key: 'uuid'
validates :uuid, uniqueness: true
end
(uuid
is core to Event Sourced Model, so please don't remove its validations.)
The calculator is a service class that idempotently calculates the state of the projection by running one method for each event, in order. Name the methods advance_[event_type]
.
class SubscriptionCalculator < EventSourcedRecord::Calculator
events :subscription_events
def advance_creation(event)
@subscription.user_id = event.user_id
@subscription.bottles_per_shipment = event.bottles_per_shipment
@subscription.bottles_left = event.bottles_purchased
end
def advance_change_settings(event)
@subscription.bottles_per_shipment = event.bottles_per_shipment
end
end
Calculators can also include other associated models, which can come in handy (as long as those models don't change significantly after creation). Add that class to events
and name the advance method advance_[underscored class name]
:
class SubscriptionCalculator < EventSourcedRecord::Calculator
events :subscription_events, :shipments
def advance_shipment(shipment)
@subscription.bottles_left -= shipment.num_bottles
end
end
The observer simply tracks creations of events related to the projection and runs the calculator every time. You may never have to modify what the generator creates for you:
class SubscriptionEventObserver < ActiveRecord::Observer
observe :subscription_event
def after_create(event)
SubscriptionCalculator.new(event).run.save!
end
end
If you use other models as events, simply add them to the observe
method:
class SubscriptionEventObserver < ActiveRecord::Observer
observe :subscription_event, :shipment
The Rake file gives you a convenient command to rebuild every projection whenever necessary. Since you'll be building the calculator to be idempotent, this will be a fairly safe operation.
namespace :subscription do
task :recalculate => :environment do
Subscription.all.each do |subscription|
SubscriptionCalculator.new(subscription).run.save!
end
end
end
- Fork it ( https://github.com/[my-github-username]/event_sourced_record/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request