ddd, rails and persistence

22
DDD, Rails and persistence Michał Łomnicki January, 2016 DRUG 1 / 21

Upload: michal-lomnicki

Post on 15-Apr-2017

705 views

Category:

Software


0 download

TRANSCRIPT

Page 1: DDD, Rails and persistence

DDD, Rails and persistenceMichał Łomnicki

January, 2016

DRUG

1 / 21

Page 2: DDD, Rails and persistence

Inspiration

Blog

https://vaughnvernon.co

Ideal DDD Aggregate Store

Book

2 / 21

Page 3: DDD, Rails and persistence

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

Page 4: DDD, Rails and persistence

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

Page 5: DDD, Rails and persistence

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

Page 6: DDD, Rails and persistence

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

Page 7: DDD, Rails and persistence

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

Page 8: DDD, Rails and persistence

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

Page 9: DDD, Rails and persistence

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

Page 10: DDD, Rails and persistence

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

Page 11: DDD, Rails and persistence

Solution 1

Data Mapper

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

11 / 21

Page 12: DDD, Rails and persistence

Solution 2

Events as a storage mechanism

Yes, that's a good solutionBig mental model change

12 / 21

Page 13: DDD, Rails and persistence

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

Page 14: DDD, Rails and persistence

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

Page 15: DDD, Rails and persistence

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

Page 16: DDD, Rails and persistence

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

16 / 21

Page 17: DDD, Rails and persistence

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

Page 18: DDD, Rails and persistence

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

Page 19: DDD, Rails and persistence

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

Page 20: DDD, Rails and persistence

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

Page 21: DDD, Rails and persistence

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

Page 22: DDD, Rails and persistence