From ActiveRecord to
Events
Emanuele DelBono
@emadb
Customer
Address
Invoice
Items
Contacts
Role
Contract
Price
City
@emadb
I’m a software developer based
in Italy. I develop my apps in C#,
Javascript and some Ruby.
I’m a wannabe Ruby dev.
Lasagna architecture
View
Controller
Model (AR)
Database
O/RM
Active record
“An object that wraps a row in a database table or
view, encapsulates the database access, and adds
domain logic on that data.”
M. Fowler
Active record
1 table => 1 class
Too coupled with the database structure
No SRP
Database first
class User < ActiveRecord::Base
attr_accessible :email, :password
belongs_to :group
end
Get vs Post
def index
@products = Product.all
end
!
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render action: 'new'
end
end
Active Record
A single model cannot be appropriate for
reporting, searching and transactional
behaviour.
Greg Young
Read vs Write
Reads and writes are two different concerns
Reads are simpler
Writes need logic
KEEP CALM
AND
STOP THINKING
IN CRUD
Command
Query
Responsibility
Segregation
CQRS
Presentation Layer
Handler
BL Repository
Write DB Read DB
Query Service
Denormalizer
CQRS
Fully normalized
Reads are easy
Writes became easier
SELECT fields FROM table (WHERE …)
Object state
id basket_id article_id quantity
1 4 8 1
2 3 8 3
3 3 6 1
4 4 5 1
Thinking in events
Every change is an event.
add_item
1
add_item
2
remove_item
1
add_item
3
time
Event Sourcing
Capture all changes to an application state as a
sequence of events.
M.Fowler
If the changes are stored in a database, we can rebuild the
state re-applying the events.
Event Sourcing
Presentation Layer
Bus
Handler
DMRepository
Event store Denormalizer
Query service
Read DB
Command
Events
Pros
• Encapsulation
• Separation of concern
• Simple storage
• Performance/Scaling
• Simple testing
• More information granularity
• Easy integration with other services
Cons
• Complex for simple scenarios
• Cost of infrastructure
• Long-living objects needs time to be reconstructed
• Tools needed (i.e. rebuild the state)
http://antwonlee.com/
Ingredients
• Rails app (no ActiveRecord)
• Redis (pub-sub)
• Sequel for querying data
• MongoDb (event store)
• Sqlite (Read db)
• Wisper (domain events)
Domain Model
Basket
BasketItem
Article
*
1
• Fully encapsulated (no accessors)
• Fully OOP
• Events for communication
• PORO
show_me_the_code.rb
include CommandExecutor
!
def add_to_basket
send_command AddToBasketCommand.new(
{"basket_id" => 42, "article_id" => params[:id].to_i})
redirect_to products_url
end
Controller
Bus
Command
POST /Products
add_to_basket
module CommandExecutor
!
def send_command (command)
class_name = command.class.name
channel = class_name.sub(/Command/, '')
@redis.publish channel, command.to_json
end
!
end
send_command
def consume(data)
basket = repository.get_basket(data["basket_id"])
article = repository.get_article(data["article_id"])
basket.add_item article
!
basket.commit
end
Bus
Handler
Command
handler
class Basket
include AggregateRootHelper
!
def add_item (item)
raise_event :item_added, {
basket_id: id,
item_code: item.code,
item_price: item.price
}
end
# ...
!
end
add_item
def raise_event(event, args)
@uncommited_events << {name: event, args: args}
send "on_#{event}", args
end
raise_event
DM (Basket)
Events
def get_item (item_code)
@items.select{|i| i.item_code == item_code}.try :first
end
!
def on_item_added (item)
get_item(item[:item_code]).try(:increase_quantity) ||
@items << BasketItem.new(item)
end
on_item_added
DM (Basket)
Events
def commit
while event = uncommited_events.shift
events_repository.store(id, event)
send_event event
end
end
commit
DM (Basket)
Event store
Event Store
def item_added(data)
db = Sequel.sqlite(AppSettings.sql_connection)
article = db[:products_view].where(code:
data[:item_code]).first
basket = db[:basket_view].where('basket_id = ? AND
article_id = ?', data[:basket_id], article[:id].to_i).first
if basket.nil?
#insert
else
db[:basket_view].where(id:
basket[:id]).update(quantity: (basket[:quantity] + 1))
end
end
denormalizer
Denormalizer
Read-Db
def index
@products=db[:basket_view]
end
Controller
Read DB
Query
GET /Products
index
Conclusion
• Stop thinking in CRUD
• Read and Write are different
• Domain model should be based on PORO
• CQRS/ES is useful in complex scenario
• Ruby power helps a lot (less infrastructure code)
https://github.com/emadb/revents

From ActiveRecord to EventSourcing