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 maintainErrorpronePoor 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 ACIDcompliantTransactions 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 modelDenormalized 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/ImplementingDomainDrivenDesignVaughnVernon/dp/0321834577
http://www.postgresql.org/docs/9.4/static/datatypejson.html
21 / 21