DDD, Rails and persistence

Post on 15-Apr-2017

702 views 0 download

Transcript of DDD, Rails and persistence

DDD, Rails and persistenceMichał Łomnicki

January, 2016

DRUG

1 / 21

Inspiration

Blog

https://vaughnvernon.co

Ideal DDD Aggregate Store

Book

2 / 21

ProblemI want DDD in my Rails projectI want fast and clean testsI want to build my application around domain objects not around database schema...but I struggle with persistence and ActiveRecord gets into my way all the time

3 / 21

Modelclass Squad include Virtus.model # optional, can be PORO

MAX_FIRST_SQUAD_PLAYERS = 11

attribute :id, UUID attribute :match_id, UUID attribute :team_id, UUID attribute :formation, Formation attribute :first_squad, Set[Player] attribute :bench, Set[Player]

4 / 21

Model def remove_from_first_squad(player) raise SquadError if !first_squad.member?(player)

first_squad.delete(player) bench.add(player) end

def add_to_first_squad(player) raise SquadError if !bench.member?(player) raise SquadError if first_squad.size == MAX_FIRST_SQUAD_PLAYERS

bench.remove(player) first_squad.add(player) end

5 / 21

Model def substitute(player_off, player_on) remove_from_first_squad(player_off) add_to_first_squad(player_on)

DomainEventPublisher.publish( PlayerSubstituted.new( squad_id: id, player_off_id: player_off.id, player_on_id: player_on.id ) ) end

6 / 21

Serviceclass SquadService include TransactionSupport

def initialize(squad_repository, player_repository) @squad_repository = squad_repository @player_repository = player_repository end

def substitute(substitution_form) transaction do DomainEventPublisher.subscribe(PlayerSubstituted, SomeHandler)

player_off = player_repository.find(substitution_form.player_off_id) player_on = player_repository.find(substitution_form.player_on_id) squad = squad_repository.find(substitution_form.squad_id)

squad.substitute(player_off, player_on) squad_repository.save(squad) end end

def change_formation(formation_form) ... endend

7 / 21

Repositoryclass SquadRepository def save(squad) if squad.id update(squad) else create(squad) end end

def create(squad) SquadAR.create( match_id: squad.match_id, formation: squad.formation.to_s, squad_players: squad.first_squad.map { |player| PlayerAR.new(first_squad: true, ...) } + squad.bench.map { |player| PlayerAR.new(first_squad: false, ...) } ) end ...

8 / 21

Repository / Naive update def update(squad) record = SquadAR.find(squad.id) record.formation = squad.formation.to_s # delete and re-create associations record.squad_players = squad.first_squad.map { |player| PlayerAR.new(first_squad: true, ...) } + squad.bench.map { |player| PlayerAR.new(first_squad: false, ...) } record.save end

9 / 21

Repository / Naive update def update(squad) record = SquadAR.find(squad.id) record.formation = squad.formation.to_s # delete and re-create associations record.squad_players = squad.first_squad.map { |player| PlayerAR.new(first_squad: true, ...) } + squad.bench.map { |player| PlayerAR.new(first_squad: false, ...) } record.save end

Hard to maintainError­pronePoor performance

10 / 21

Solution 1

Data Mapper

No mature Data Mapper for RubyROM looks promising...but is yet incomplete

11 / 21

Solution 2

Events as a storage mechanism

Yes, that's a good solutionBig mental model change

12 / 21

Solution 3

Postgres + JSON

Ideal DDD Aggregate store?Aggregate data stored as JSONOne database row ­ one aggregate

create_table "squads" do |t| t.jsonb :data, null: false end

13 / 21

DB schema

create_table "users" do |t| t.jsonb :data, null: false end

create_table "matches" do |t| t.jsonb :data, null: false end

create_table "teams" do |t| t.jsonb :data, null: false end

create_table "squads" do |t| t.jsonb :data, null: false end

14 / 21

JSON Repositoryclass SquadRepository def save(squad) if squad.id update(squad) else create(squad) end end

def find(squad_id) Domain::Squad.new(SquadAR.find(squad_id)) end

private

def create(squad) SquadAR.create(data: squad.as_json) end

def update(squad) SquadAR.where(id: squad.id).update_all(data: squad.as_json) endend

15 / 21

Why not Mongo? This looks like NoSQLMongo is not ACID­compliantTransactions only at the document level

16 / 21

Postgres + JSONJSON introduced in Postgres 9.3JSONB introduced in Postgres 9.4JSONB can be indexedPostgres = ACIDNo foreign keys and unique indexesData consistency ensured at application levelIntroduce this approach in the existing database

17 / 21

Postgres + JSONJSON introduced in Postgres 9.3JSONB introduced in Postgres 9.4JSONB can be indexedPostgres = ACIDNo foreign keys and unique indexesData consistency ensured at application levelIntroduce this approach in the existing database

CREATE INDEX ON squads USING gin (data) SELECT * FROM squads WHERE (data ->> 'match_id')::INT = 12

18 / 21

LockingSome locking mechanism is requiredOptimisic locking is preferred

a) Process 1 readsb) Process 2 readsc) Process 1 writesd) Process 2 overwrites c)

SquadAR.where(id: squad.id, lock_version: current_version[squad]).update_all( data: squad.as_json, lock_version: current_version[squad] + 1 )

19 / 21

Lessons learntIt worksChanges are easy to introduceFast and easy to store and load an entire aggregateCode explains the application, not the DB schemaMigrations require more workMost probably you will build a read modelDe­normalized data needs synchronizationAvoid big aggregatesNo help from the database (foreign keys, not null, etc)Can't really fiddle in rails consoleSquad.find(123).squad_players.update_all(...)

20 / 21

Thank youResources:

https://vaughnvernon.co/?p=942

http://www.amazon.com/Implementing­Domain­Driven­Design­Vaughn­Vernon/dp/0321834577

http://www.postgresql.org/docs/9.4/static/datatype­json.html

21 / 21