Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Patterns to deal with big ActiveRecord models

Patterns to deal with big ActiveRecord models

“Skinny controllers, fat models” is a well-known practice in the Rails community that everyone seems to follow. However, as your application evolves and your models grow, maintaining them can become less enjoyable than it used to be.

In the last few years, the community has been proposing patterns and techniques to deal with big AR models. Presenters, Service objects, Concerns or DCI are only a few examples of solutions suggested to alleviate the pain caused by gigantic models.

In this talk, we will critically explore these patterns in their different flavours. We'll compare them in order to expose their strengths and weaknesses and you'll learn when and how to use them to keep your Rails models as pleasurable to deal with as the first day.

Luismi Cavallé

October 04, 2013
Tweet

More Decks by Luismi Cavallé

Other Decks in Programming

Transcript

  1. 1BUUFSOTUPEFBMXJUI CJH"DUJWF3FDPSE NPEFMT CZ-VJTNJ$BWBMMÉ

  2. None
  3. None
  4. require_dependency,'topic_view' require_dependency,'rate_limiter' require_dependency,'text_sentinel' require_dependency,'text_cleaner' class,Topic,<,ActiveRecord::Base ,,include,ActionView::Helpers ,,include,RateLimiter::OnCreateRecord ,,def,self.max_sort_order ,,,,2**31,7,1 https://github.com/discourse/discourse/blob/master/app/models/topic.rb

  5. require_dependency,'slug' require_dependency,'avatar_lookup' require_dependency,'topic_view' require_dependency,'rate_limiter' require_dependency,'text_sentinel' require_dependency,'text_cleaner' class,Topic,<,ActiveRecord::Base ,,include,ActionView::Helpers ,,include,RateLimiter::OnCreateRecord ,,def,self.max_sort_order

    ,,,,2**31,7,1 ,,end ,,def,self.featured_users_count ,,,,4 ,,end ,,versioned,if:,:new_version_required? ,,acts_as_paranoid ,,after_recover,:update_flagged_posts_count ,,after_destroy,:update_flagged_posts_count ,,rate_limit,:default_rate_limiter https://github.com/discourse/discourse/blob/master/app/models/topic.rb
  6. https://github.com/discourse/discourse/blob/master/app/models/topic.rb -0$ BOEUIBU`TOPUUPPNVDI

  7. #JH.PEFMT

  8. 'BU.PEFMT

  9. (JHBOUJD .PEFMT

  10. 8IZBSFUIFZCBE

  11. 431

  12. -PX$PIFTJPO

  13. 5JHIU$PVQMJOH

  14. #FDBVTF {{your_favourite_OO_guru}} TBZTTP

  15. 8IZBSFUIFZCBE JOQSBDUJDBMUFSNT  QMFBTF

  16. !!class!Article!<!ActiveRecord::Base !!!!attr_accessor!:moderation_option ! !!!!attr_accessible!:text,!:reply_to_i ! !!!!belongs_to!:topic !!!!belongs_to!:user,!!!!!:class_name! !!!!belongs_to!:reply_to,!:class_name!

  17. !!!belongs_to!:user,!!!!!:class_name!=>!Fore !!!belongs_to!:reply_to,!:class_name!=>!"Pos !!!has_many!:replies,!:class_name!!=>!"Post" !!!!!!!!!!!!!!!!!!!!!!:foreign_key!=>!"reply !!!!!!!!!!!!!!!!!!!!!!:dependent!!!=>!:nulli !!! !!!has_many!:taggings !!!has_many!:tags,!:through!=>!:taggings !!!validates!:text,!:presence!=>!true !!!delegate!:forum,!:to!=>!:topic

    !!!after_create!:set_topic_last_post_at !!!after_create!:skip_pending_review
  18. !!!!end !!!!def!pending_review !!!!!!where!:state!=>!'pending_review' !!!!end !!!!def!spam !!!!!!where!:state!=>!'spam' !!!!end !!!! !!!!def!tagged_with(tag) !!!!!!joins(:taggings!=>!:tags).where(:tags!=>!{!:name!=>!tag!})

    !!!!end !!!!def!visible !!!!!!joins(:topic).where(:forem_topics!=>!{!:hidden!=>!false!}) !!!!end !!end !! !!def!tag_names=(tag_list) !!!!assign_tag_list!tag_list !!end !!private
  19. !!!!!end !!!end !!! !!!def!tag_names=(tag_list) !!!!!assign_tag_list!tag_list !!!end !!!private !!!def!subscribe_replier !!!!!if!topic!&&!user

  20. ! !!!!def!blacklist_user !!!!!!user.update_attribute(:forem_state,!"spam")!if!user !!!!end !!!! !!!!def!assign_tag_list(tag_list) !!!!!!tag_names!=!tag_list.gsub(/\s+/,!"").split(",") !!!!!!existing!=!self.tags.map!{|t|!t.name!} !!!!!!(existing!@!tag_names).each!do!|name| !!!!!!!!self.tags.delete!Tag.find_by_name(name)

    !!!!!!end !!!!!!tag_names.each!do!|name| !!!!!!!!self.tags!<<!Tag.find_or_create_by_name(name) !!!!!!end! !!!!end !!end
  21. !!class!Article!<!ActiveRecord::Base !!!!attr_accessor!:moderation_option ! !!!!attr_accessible!:text,!:reply_to_id,!:tag_names ! !!!!belongs_to!:topic !!!!belongs_to!:user,!!!!!:class_name!=>!Forem.user_class.to_s !!!!belongs_to!:reply_to,!:class_name!=>!"Post" ! !!!!has_many!:replies,!:class_name!!=>!"Post",

    !!!!!!!!!!!!!!!!!!!!!!!:foreign_key!=>!"reply_to_id", !!!!!!!!!!!!!!!!!!!!!!!:dependent!!!=>!:nullify !!!! !!!!has_many!:taggings !!!!has_many!:tags,!:through!=>!:taggings ! !!!!validates!:text,!:presence!=>!true ! !!!!delegate!:forum,!:to!=>!:topic ! !!!!after_create!:set_topic_last_post_at !!!!after_create!:skip_pending_review !!!!after_save!:email_topic_subscribers ! !!!!class!<<!self !!!!!!def!by_created_at !!!!!!!!order!:created_at !!!!!!end ! !!!!!!def!pending_review !!!!!!!!where!:state!=>!'pending_review' !!!!!!end ! !!!!!!def!spam !!!!!!!!where!:state!=>!'spam' !!!!!!end !!!!!! !!!!!!def!tagged_with(tag) !!!!!!!!joins(:taggings!=>!:tags).where(:tags!=>!{!:name!=>!tag!}) !!!!!!end ! !!!!!!def!visible !!!!!!!!joins(:topic).where(:forem_topics!=>!{!:hidden!=>!false!}) !!!!!!end !!!!end !!!! !!!!def!tag_names=(tag_list) !!!!!!assign_tag_list!tag_list !!!!end ! !!!!private ! !!!!def!subscribe_replier !!!!!!if!topic!&&!user !!!!!!!!topic.subscribe_user(user.id) !!!!!!end !!!!end ! !!!!def!set_topic_last_post_at !!!!!!topic.update_attribute(:last_post_at,!created_at) !!!!end ! !!!!def!blacklist_user !!!!!!user.update_attribute(:forem_state,!"spam")!if!user !!!!end !!!! !!!!def!assign_tag_list(tag_list) !!!!!!tag_names!=!tag_list.gsub(/\s+/,!"").split(",") !!!!!!existing!=!self.tags.map!{|t|!t.name!} !!!!!!(existing!@!tag_names).each!do!|name| !!!!!!!!self.tags.delete!Tag.find_by_name(name) !!!!!!end !!!!!!tag_names.each!do!|name| !!!!!!!!self.tags!<<!Tag.find_or_create_by_name(name) !!!!!!end! !!!!end !!end
  22. before_validation after_validation before_save before_create after_create after_save after_commit

  23. 8IBUDBOXFEP BCPVUJU

  24. .PEFMT

  25. .PEFM

  26. .PEFM .PEFM .PEFM

  27. has_one

  28. 4DIFEVMJOH

  29. create_table)"schedulings")do)|t| ))#"... ))t.integer))"workflow_offset" ))t.string)))"workflow_asset_url" ))t.text)))))"workflow_details" ))t.boolean))"workflow_sent" ))t.string)))"workflow_web_url" ))t.string)))"workflow_template_url"

  30. class)Scheduling)<)ActiveRecord::Base ))#"... ))include)Workflow

  31. class)SchedulingsController)<)ApplicationController class)WorkflowsController)<)ApplicationController

  32. 4DIFEVMJOH

  33. 4DIFEVMJOH 8PSLqPX

  34. class)Workflow)<)ActiveRecord::Base ))belongs_to):scheduling

  35. class)Scheduling)<)ActiveRecord::Base ))#"... ))has_one):workflow

  36. .BEFFYQMJDJUBO JNQMJDJUFOUJUZPGUIF EPNBJO

  37. .PEFMTBSFTNBMMFS

  38. /POQFSTJTUFE NPEFMT

  39. ActiveModel

  40. 'PSNPCKFDUT

  41. 1SPKFDUT $PNQBOZ 6TFST 4JHOVQ "1*5PLFOT

  42. class!Signup !!extend!ActiveModel::Naming !!include!ActiveModel::Conversion !!include!ActiveModel::Validations !!attr_accessor!:name !!attr_accessor!:company_name !!attr_accessor!:email !!validates!:email,!presence:!true !!#"…"more"validations"… !!#"Virtual"models"are"never"themselves"persisted

    !!def!persisted? !!!!false !!end !!def!save !!!!if!valid? !!!!!!persist! !!!!!!true !!!!else !!!!!!false !!!!end !!end private !!def!persist! !!!!@company!=!Company.create!(name:!company_name) !!!!@user!=!@company.users.create!(name:!name,!email:!email) !!end end class!SignupsController!<!ApplicationController !!def!create http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  43. class)SignupsController)<)ApplicationController ))def)create ))))@signup)=)Signup.new(params[:signup]) ))))if)@signup.save ))))))redirect_to)dashboard_path ))))else ))))))render)"new" ))))end ))end end

    http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  44. class)SimpleModel ))extend)ActiveModel::Naming ))include)ActiveModel::Conversion ))include)ActiveModel::Validations ))#"Virtual"models"are"never"themselves"persisted ))def)persisted? ))))false ))end ))def)save ))))if)valid?

    ))))))persist! ))))))true ))))else ))))))false ))))end ))end end
  45. class)Signup)<)SimpleModel ))attr_accessor):name, ))HHHHHHHHHHHHHH:company_name, ))HHHHHHHHHHHHHH:email ))validates):email,)presence:)true ))#"…"more"validations"… ))def)persist! ))))@company)=)Company.create!(name:)company_name) ))))@user)=)@company.users.create!(name:)name,)email:)email) ))end

    end
  46. $PODFSOT

  47. module)DogFort ))def)call_dog ))))puts)"this)is)dog!" ))end end class)Dog ))include)DogFort end http://schneems.com/post/21380060358/concerned-about-code-reuse

  48. ActiveSupport::Concern

  49. module)M ))extend)ActiveSupport::Concern )))) ))included)do ))))scope):disabled,)where(:disabled)=>)true) ))end )) ))module)ClassMethods ))))#"class"methods ))end

    ))#"instance"methods end http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
  50. class)Article)<)ActiveRecord::Base ))include)Taggable

  51. module!Taggable !!extend!ActiveSupport::Concern !!included!do !!!!has_many!:taggings !!!!has_many!:tags,!through:!:taggings !!!!scope!:tagged_with,!+>(tag_name)!do !!!!!!joins(:tags).where(tags:!{!name:!tag_name!})! !!!!end !!end !!def!tag_list

    !!!!tags.pluck(:name).join(',!') !!end !!def!tag_list=(tag_list) !!!!assign_tag_list!tag_list !!end !!private !!def!assign_tag_list(tag_list) !!!!tag_names!=!tag_list.split(',').map(&:strip).uniq !!!!self.tags!=!tag_names.map!do!|tag_name|! !!!!!!Tag.where(name:!tag_name).first_or_initialize !!!!end !!end end
  52. 5IFZIFMQUPLFFQ DPIFTJPOIJHI

  53. %3:OFTT

  54. *OUSPEVDFMFTT JOEJSFDUJPOBOE DFSFNPOZUIBO PUIFSBCTUSBDUJPOT DPNQPTJUJPO EFDPSBUJPO

  55. l0⒏DJBMTPMVUJPOzUP UIF#JH.PEFMTJTTVF JO3BJMT

  56. app/controllers/concerns app/models/concerns

  57. %PO`UpYUIFQSPCMFN KVTU IJEFJUCZNPWJOHDPEF BSPVOE 0CKFDUTFOEVQXJUIUIF TBNFNFUIPETBOE SFTQPOTJCJMJUJFT

  58. /BNJOHDPOqJDUT 4IBSFETUBUF #PVOEBSJFTOPFYQMJDJU

  59. $PNNPOQVSQPTF /PUCBTJDOBUVSFPGNPEFM $SPTTDVUUJOH %PNBJODPODFQU

  60. #"app/models/article.rb class)Article)<)ActiveRecord::Base ))include)Accessors ))include)Validations ))include)Associations ))include)Scopes ))#"... end

  61. #"app/models/article.rb class)Article)<)ActiveRecord::Base ))include)Taggable ))include)Searchable ))include)Movable ))include)Visible ))include)Trashable ))#"... end

  62. 3FTQPOTJCJMJUJFT

  63. http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ class)UserAuthenticator ))def)initialize(user) ))))@user)=)user ))end ))def)authenticate(unencrypted_password) ))))return)false)unless)@user ))))if)BCrypt::Password.new(@user.password_digest))==)unencrypted_password ))))))@user ))))else

    ))))))false ))))end ))end end
  64. user)=)User.where(email:)params[:email]).first UserAuthenticator.new(user).authenticate(params[:password])

  65. user)=)User.where(email:)params[:email]).first UserAuthenticator.new(user).authenticate(params[:password])

  66. https://gist.github.com/brynary/4670393 user!=!User.where(email:!params[:email]).first UserAuthenticator.new(user).authenticate(params[:password]) class!ActiveUserPolicy !!LOGIN_PERIOD!=!14.days !! !!def!initialize(user) !!!!@user!=!user !!end !!def!active?

    !!!!@user.email_confirmed?!&& !!!!@user.last_login_at!<!LOGIN_PERIOD.ago !!end !!class!Query !!!!def!initialize(relation!=!User.scoped) !!!!!!@relation!=!relation !!!!end !!!!def!find_each(&block) !!!!!!@relation. !!!!!!!!where(email_confirmed:!true). !!!!!!!!where('last_login!<!?',!LOGIN_PERIOD.ago). !!!!!!!!find_each(&block) !!!!end !!end end ActiveUserPolicy.active?(user)
  67. ActiveUserPolicy.new(user).active? ActiveUserPolicy::Query.new(current_company.users).find_each)do)|user| ))#"... end

  68. 4JOHMFSFTQPOTJCJMJUZ &YQMJDJUCPVOEBSJFT -JHIUFSUFTUT .PSFJOEJSFDUJPOBOEDFSFNPOZ

  69. module)User::Active ))extend)ActiveSupport::Concern ))included)do ))))scope):active,)M>)do ))))))where(email_confirmed:)true). ))))))where('last_login)<)?',)14.days.ago) ))))end ))end ))def)active? ))))user.email_confirmed?)&&

    ))))user.last_login_at)<)14.days.ago ))end end https://gist.github.com/cavalle/4660239
  70. https://gist.github.com/brynary/4670393 user!=!User.where(email:!params[:email]).first UserAuthenticator.new(user).authenticate(params[:password]) class!ActiveUserPolicy !!LOGIN_PERIOD!=!14.days !! !!def!initialize(user) !!!!@user!=!user !!end !!def!active?

    !!!!@user.email_confirmed?!&& !!!!@user.last_login_at!<!LOGIN_PERIOD.ago !!end !!class!Query !!!!def!initialize(relation!=!User.scoped) !!!!!!@relation!=!relation !!!!end !!!!def!find_each(&block) !!!!!!@relation. !!!!!!!!where(email_confirmed:!true). !!!!!!!!where('last_login!<!?',!LOGIN_PERIOD.ago). !!!!!!!!find_each(&block) !!!!end !!end end ActiveUserPolicy.active?(user)
  71. user.active? current_company.users.active.each)do)|user| ))#"... end

  72. ActiveUserPolicy.active?(user) ActiveUserPolicy::Query.new(current_company.users).find_each)do)|user| ))#"... end

  73. 3PMFT

  74. class)Commenter ))def)initialize(person) ))))@person)=)person ))end ))def)recent_comments ))))@person.comments.where("created_at)>)?",)3.days.ago) ))end ))def)post_comment(text) ))))@person.comments.create(:body)=>)text) ))end

    ))def)update_comment(new_text) ))))raise)"Comment)not)owned)by)this)person")unless)comment.author)==)@person ))))comment.update_attribute(:body,)new_text) ))end end https://gist.github.com/4341122
  75. class)Commenter ))def)initialize(person) ))))@person)=)person ))end ))def)recent_comments ))))@person.comments.where("created_at)>)?",)3.days.ago) ))end ))def)post_comment(text) ))))@person.comments.create(:body)=>)text) ))end

    ))def)update_comment(new_text) ))))raise)"Comment)not)owned)by)this)person")unless)comment.author)==)@person ))))comment.update_attribute(:body,)new_text) ))end end https://gist.github.com/4341122
  76. (BUFXBZT

  77. http://slides.jcoglan.com/di-eurucamp#13 class)Github::Client ))def)initialize(http_client) ))))@http)=)http_client ))end ))def)get_user(name) ))))data)=)@http.get("/users/#{name}").json_data ))))Github::User.new(data) ))end end

  78. http_client)=)HTTPClient.new('https://api.github.com') github)=)Github::Client.new(http_client) github.get_user(params[:name])

  79. TwitterPoster.new(current_user,)@comment.body).post FacebookPoster.new(current_user,)@comment.body).post NotificationsMailer.new_comment(@comment).deliver

  80. 5IJOFSNPEFMT &YQMJDJUJOUFSGBDFCFUXFFO PVSEPNBJOBOEFYUFSOBM TZTUFNT

  81. $POUSPMMFST

  82. :FT DPOUSPMMFST

  83. http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model

  84. 4LJOOJFTUQPTTJCMF DPOUSPMMFST

  85. class)CommentsController)<)ApplicationController ))#... ))def)create ))))@comment)=)@post.comments.build(post_params) ))))if)@comment.save ))))))redirect_to)@comment ))))else ))))))render)action:)'new' ))))end ))end

  86. https://gist.github.com/2838490#gistcomment-356060

  87. class)CommentsController)<)ApplicationController ))#... )) ))def)create ))))@comment)=)@post.comments.create!(post_params) )))) ))))Notifications.new_comment(@comment).deliver )))) ))))TwitterPoster.new(current_user,)@comment.body).post ))))FacebookPoster.new(current_user,)@comment.body).post

    ))end
  88. *U`TPLUPVTF DPOUSPMMFSTGPS PSDIFTUSBUJPO

  89. "WPJEDBMMCBDLT

  90. %PO`UQVUBOZEPNBJOMPHJDJO UIFDPOUSPMMFS&ODBQTVMBUFJU JOEPNBJOPCKFDUT NPEFMT  NBJMFST HBUFXBZT

  91. $POUFYUT

  92. class!PostComment !!def!initialize(user,!entry,!attributes) !!!!@user!=!user !!!!@entry!=!entry !!!!@attributes!=!attributes !!end ! !!def!post !!!!@comment!=!@user.comments.new !!!!@comment.assign_attributes(@attributes)

    !!!!@comment.entry!=!@entry !!!!@comment.save! ! !!!!LanguageDetector.new(@comment).set_language !!!!SpamChecker.new(@comment).check_spam !!!!CommentMailer.new(@comment).send_mail ! !!!!post_to_twitter!!if!@comment.share_on_twitter? !!!!post_to_facebook!if!@comment.share_on_facebook? ! !!!!@comment !!end ! !!private ! !!def!post_to_twitter !!!!PostToTwitter.new(@user,!@comment).post !!end ! !!def!post_to_facebook !!!!PostToFacebook.new(@user,!@comment).action(:comment) !!end end class!CommentsController!<!ApplicationController https://gist.github.com/2838490
  93. class)CommentsController)<)ApplicationController ))#... )) ))def)create ))))context)=)PostComment.new(current_user,)@post,)post_params) ))))@comment)=)context.post ))end

  94. -FTTDBMMCBDLT 4JOHMFSFTQPOTJCJMJUZ -JHIUFSUFTUT .PSFJOEJSFDUJPO .PSFDFSFNPOZ

  95. 1VUUJOHJUUPHFUIFS

  96. .PSFEPNBJONPEFMT &YUSBDUJOHSFTQPOTJCJMJUJFT &YQMJDJUFYUFSOBMJOUFSBDUJPOT $PPSEJOBUJOHGSPNBCPWF

  97. 4UZMFT

  98. 3BJMT8BZ

  99. $POUSPMMFSTPWFS$POUFYUT $PODFSOTPWFS0CKFDUT (BUFXBZT "3GPSCBTJDEPNBJOMPHJD

  100. 00XBZ

  101. $POUFYUTPWFS$POUSPMMFST 0CKFDUTPWFS$PODFSOT (BUFXBZT "3GPSQFSTJTUFODFPOMZ

  102. %$*XBZ

  103. $POUFYUTGPSFBDI6TF$BTF 3PMFTGPSBOZCVTJOFTTMPHJD "3GPSEBUBBDDFTTPOMZ

  104. module)Customer ))def)add_to_cart(book) ))))self.cart)<<)book ))end end http://mikepackdev.com/blog_posts/24-the-right-way-to-code-dci-in-ruby

  105. http://mikepackdev.com/blog_posts/24-the-right-way-to-code-dci-in-ruby class)AddToCart ))def)initialize(user,)book) ))))@user,)@book)=)user,)book ))))@user.extend)Customer ))end ))def)call ))))@user.add_to_cart(@book) ))end end

  106. 5IF3BJMTXBZ 5IF00XBZ 5IF%$*XBZ

  107. :PVSXBZ

  108. %BOL6XFM -VJTNJ$BWBMMÉ UXJUUFSDPNDBWBMMF HJUIVCDPNDBWBMMF QJOCPBSEJOVDBWBMMFUCJHNPEFMT