Share
Embed
DDD, Rails and persistence
Michał Łomnicki
January 18, 2016
Transcript
22 slides
-
Problem I want DDD in my Rails project I want
fast and clean tests I 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 -
Model c l a s s S q u a
d i n c l u d e V i r t u s . m o d e l # o p t i o n a l , c a n b e P O R O M A X _ F I R S T _ S Q U A D _ P L A Y E R S = 1 1 a t t r i b u t e : i d , U U I D a t t r i b u t e : m a t c h _ i d , U U I D a t t r i b u t e : t e a m _ i d , U U I D a t t r i b u t e : f o r m a t i o n , F o r m a t i o n a t t r i b u t e : f i r s t _ s q u a d , S e t [ P l a y e r ] a t t r i b u t e : b e n c h , S e t [ P l a y e r ] 4 / 21 -
Model d e f r e m o v e
_ f r o m _ f i r s t _ s q u a d ( p l a y e r ) r a i s e S q u a d E r r o r i f ! f i r s t _ s q u a d . m e m b e r ? ( p l a y e r ) f i r s t _ s q u a d . d e l e t e ( p l a y e r ) b e n c h . a d d ( p l a y e r ) e n d d e f a d d _ t o _ f i r s t _ s q u a d ( p l a y e r ) r a i s e S q u a d E r r o r i f ! b e n c h . m e m b e r ? ( p l a y e r ) r a i s e S q u a d E r r o r i f f i r s t _ s q u a d . s i z e = = M A X _ F I R S T _ S Q U A D _ P L A Y E R S b e n c h . r e m o v e ( p l a y e r ) f i r s t _ s q u a d . a d d ( p l a y e r ) e n d 5 / 21 -
Model d e f s u b s t i
t u t e ( p l a y e r _ o f f , p l a y e r _ o n ) r e m o v e _ f r o m _ f i r s t _ s q u a d ( p l a y e r _ o f f ) a d d _ t o _ f i r s t _ s q u a d ( p l a y e r _ o n ) D o m a i n E v e n t P u b l i s h e r . p u b l i s h ( P l a y e r S u b s t i t u t e d . n e w ( s q u a d _ i d : i d , p l a y e r _ o f f _ i d : p l a y e r _ o f f . i d , p l a y e r _ o n _ i d : p l a y e r _ o n . i d ) ) e n d 6 / 21 -
Service c l a s s S q u a
d S e r v i c e i n c l u d e T r a n s a c t i o n S u p p o r t d e f i n i t i a l i z e ( s q u a d _ r e p o s i t o r y , p l a y e r _ r e p o s i t o r y ) @ s q u a d _ r e p o s i t o r y = s q u a d _ r e p o s i t o r y @ p l a y e r _ r e p o s i t o r y = p l a y e r _ r e p o s i t o r y e n d d e f s u b s t i t u t e ( s u b s t i t u t i o n _ f o r m ) t r a n s a c t i o n d o D o m a i n E v e n t P u b l i s h e r . s u b s c r i b e ( P l a y e r S u b s t i t u t e d , S o m e H a n d l e r ) p l a y e r _ o f f = p l a y e r _ r e p o s i t o r y . f i n d ( s u b s t i t u t i o n _ f o r m . p l a y e r _ o f f _ i d ) p l a y e r _ o n = p l a y e r _ r e p o s i t o r y . f i n d ( s u b s t i t u t i o n _ f o r m . p l a y e r _ o n _ i d ) s q u a d = s q u a d _ r e p o s i t o r y . f i n d ( s u b s t i t u t i o n _ f o r m . s q u a d _ i d ) s q u a d . s u b s t i t u t e ( p l a y e r _ o f f , p l a y e r _ o n ) s q u a d _ r e p o s i t o r y . s a v e ( s q u a d ) e n d e n d d e f c h a n g e _ f o r m a t i o n ( f o r m a t i o n _ f o r m ) . . . e n d e n d 7 / 21 -
Repository c l a s s S q u a
d R e p o s i t o r y d e f s a v e ( s q u a d ) i f s q u a d . i d u p d a t e ( s q u a d ) e l s e c r e a t e ( s q u a d ) e n d e n d d e f c r e a t e ( s q u a d ) S q u a d A R . c r e a t e ( m a t c h _ i d : s q u a d . m a t c h _ i d , f o r m a t i o n : s q u a d . f o r m a t i o n . t o _ s , s q u a d _ p l a y e r s : s q u a d . f i r s t _ s q u a d . m a p { | p l a y e r | P l a y e r A R . n e w ( f i r s t _ s q u a d : t r u e , . . . ) } + s q u a d . b e n c h . m a p { | p l a y e r | P l a y e r A R . n e w ( f i r s t _ s q u a d : f a l s e , . . . ) } ) e n d . . . 8 / 21 -
Repository / Naive update d e f u p d
a t e ( s q u a d ) r e c o r d = S q u a d A R . f i n d ( s q u a d . i d ) r e c o r d . f o r m a t i o n = s q u a d . f o r m a t i o n . t o _ s # d e l e t e a n d r e - c r e a t e a s s o c i a t i o n s r e c o r d . s q u a d _ p l a y e r s = s q u a d . f i r s t _ s q u a d . m a p { | p l a y e r | P l a y e r A R . n e w ( f i r s t _ s q u a d : t r u e , . . . ) } + s q u a d . b e n c h . m a p { | p l a y e r | P l a y e r A R . n e w ( f i r s t _ s q u a d : f a l s e , . . . ) } r e c o r d . s a v e e n d 9 / 21 -
Repository / Naive update d e f u p d
a t e ( s q u a d ) r e c o r d = S q u a d A R . f i n d ( s q u a d . i d ) r e c o r d . f o r m a t i o n = s q u a d . f o r m a t i o n . t o _ s # d e l e t e a n d r e - c r e a t e a s s o c i a t i o n s r e c o r d . s q u a d _ p l a y e r s = s q u a d . f i r s t _ s q u a d . m a p { | p l a y e r | P l a y e r A R . n e w ( f i r s t _ s q u a d : t r u e , . . . ) } + s q u a d . b e n c h . m a p { | p l a y e r | P l a y e r A R . n e w ( f i r s t _ s q u a d : f a l s e , . . . ) } r e c o r d . s a v e e n d Hard to maintain Errorprone Poor performance 10 / 21 -
Solution 1 Data Mapper No mature Data Mapper for Ruby
ROM looks promising ...but is yet incomplete 11 / 21 -
Solution 2 Events as a storage mechanism Yes, that's a
good solution Big mental model change 12 / 21 -
Solution 3 Postgres + JSON Ideal DDD Aggregate store? Aggregate
data stored as JSON One database row one aggregate c r e a t e _ t a b l e " s q u a d s " d o | t | t . j s o n b : d a t a , n u l l : f a l s e e n d 13 / 21 -
DB schema c r e a t e _ t
a b l e " u s e r s " d o | t | t . j s o n b : d a t a , n u l l : f a l s e e n d c r e a t e _ t a b l e " m a t c h e s " d o | t | t . j s o n b : d a t a , n u l l : f a l s e e n d c r e a t e _ t a b l e " t e a m s " d o | t | t . j s o n b : d a t a , n u l l : f a l s e e n d c r e a t e _ t a b l e " s q u a d s " d o | t | t . j s o n b : d a t a , n u l l : f a l s e e n d 14 / 21 -
JSON Repository c l a s s S q u
a d R e p o s i t o r y d e f s a v e ( s q u a d ) i f s q u a d . i d u p d a t e ( s q u a d ) e l s e c r e a t e ( s q u a d ) e n d e n d d e f f i n d ( s q u a d _ i d ) D o m a i n : : S q u a d . n e w ( S q u a d A R . f i n d ( s q u a d _ i d ) ) e n d p r i v a t e d e f c r e a t e ( s q u a d ) S q u a d A R . c r e a t e ( d a t a : s q u a d . a s _ j s o n ) e n d d e f u p d a t e ( s q u a d ) S q u a d A R . w h e r e ( i d : s q u a d . i d ) . u p d a t e _ a l l ( d a t a : s q u a d . a s _ j s o n ) e n d e n d 15 / 21 -
Why not Mongo? This looks like NoSQL Mongo is not
ACIDcompliant Transactions only at the document level 16 / 21 -
Postgres + JSON JSON introduced in Postgres 9.3 JSONB introduced
in Postgres 9.4 JSONB can be indexed Postgres = ACID No foreign keys and unique indexes Data consistency ensured at application level Introduce this approach in the existing database 17 / 21 -
Postgres + JSON JSON introduced in Postgres 9.3 JSONB introduced
in Postgres 9.4 JSONB can be indexed Postgres = ACID No foreign keys and unique indexes Data consistency ensured at application level Introduce this approach in the existing database C R E A T E I N D E X O N s q u a d s U S I N G g i n ( d a t a ) S E L E C T * F R O M s q u a d s W H E R E ( d a t a - > > ' m a t c h _ i d ' ) : : I N T = 1 2 18 / 21 -
Locking Some locking mechanism is required Optimisic locking is preferred
a ) P r o c e s s 1 r e a d s b ) P r o c e s s 2 r e a d s c ) P r o c e s s 1 w r i t e s d ) P r o c e s s 2 o v e r w r i t e s c ) S q u a d A R . w h e r e ( i d : s q u a d . i d , l o c k _ v e r s i o n : c u r r e n t _ v e r s i o n [ s q u a d ] ) . u p d a t e _ a l l ( d a t a : s q u a d . a s _ j s o n , l o c k _ v e r s i o n : c u r r e n t _ v e r s i o n [ s q u a d ] + 1 ) 19 / 21 -
Lessons learnt It works Changes are easy to introduce Fast
and easy to store and load an entire aggregate Code explains the application, not the DB schema Migrations require more work Most probably you will build a read model Denormalized data needs synchronization Avoid big aggregates No help from the database (foreign keys, not null, etc) Can't really fiddle in r a i l s c o n s o l e S q u a d . f i n d ( 1 2 3 ) . s q u a d _ p l a y e r s . u p d a t e _ a l l ( . . . ) 20 / 21