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.

8a8d65bb7cec6df5ee7383532145b400?s=128

Luismi Cavallé

October 04, 2013
Tweet

Transcript

  1. 2.
  2. 3.
  3. 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
  4. 11.

    431

  5. 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
  6. 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
  7. 24.
  8. 25.
  9. 27.
  10. 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/
  11. 46.
  12. 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
  13. 53.
  14. 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)
  15. 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)
  16. 73.
  17. 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
  18. 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
  19. 76.
  20. 91.
  21. 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
  22. 97.
  23. 98.
  24. 100.
  25. 102.
  26. 107.