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. 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
  2. 431

  3. !!!!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
  4. !!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
  5. 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) !!!!@[email protected]!(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/
  6. 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
  7. 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?

    [email protected]_confirmed?!&& [email protected]_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)
  8. 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?

    [email protected]_confirmed?!&& [email protected]_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)
  9. 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
  10. 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
  11. class!PostComment !!def!initialize(user,!entry,!attributes) !!!!@user!=!user !!!!@entry!=!entry !!!!@attributes!=!attributes !!end ! !!def!post !!!!@[email protected] [email protected]_attributes(@attributes)

    [email protected]!=!@entry [email protected]! ! !!!!LanguageDetector.new(@comment).set_language !!!!SpamChecker.new(@comment).check_spam !!!!CommentMailer.new(@comment).send_mail ! [email protected]_on_twitter? [email protected]_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