From ActiveRecord to EventSourcing

Post on 14-May-2015

4897 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

DESCRIPTION

An introduction to a possible implementation of CQRS/ES architecture for a Ruby on Rails app. It starts from Domain Model to arrive to a sample app that implements the Event Sourcing pattern. This presentation was part of Wroclove_rb 2014 conference in Wraclow (PL)

Transcript

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

top related