Date post: | 14-May-2015 |
Category: |
Technology |
Upload: | emanuele-delbono |
View: | 4,897 times |
Download: | 0 times |
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 record1 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 SourcingPresentation 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 ModelBasket
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
BusCommand
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
HandlerCommand
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
commitDM (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 DBQuery
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