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

Starving ActiveRecord

Starving ActiveRecord

Some techniques to deal with fat ActiveRecord models.

Radoslav Stankov

May 21, 2015
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. • Value objects • Service objects • Form objects •

    Query objects • Presenter/Decorator objects • Page object • Helper objects
  2. class Size include Comparable SIZES = [ s: 'small' m:

    'medium' l: 'large' xl: 'XL' xxl: 'XXL' xxxl: 'XXL' ] GENDERS = [ f: 'female', m: 'male', u: 'unisex' ] GENDER_KEYS = SIZES.keys SIZE_KEYS = SIZES.keys attr_reader :size, :gender class << self def groups SIZE_KEYS.map(&:to_s) end def genders GENDER_KEYS.map(&:to_s) end end def initialize(size, gender) @size = size.to_sym @gender = gender.to_sym
  3. class << self def groups SIZE_KEYS.map(&:to_s) end def genders GENDER_KEYS.map(&:to_s)

    end end def initialize(size, gender) @size = size.to_sym @gender = gender.to_sym raise ArgumentError unless SIZES[@size].present? raise ArgumentError unless GENDERS[@gender].present? end def name "#{GENDERS[gender]} #{SIZES[size]}" end def <=>(other) [GENDER_KEYS.index(gender), SIZE_KEYS.index(size)] <=> [GENDER_KEYS.index(other.gender), SIZE_KEYS.i end def eql?(other) gender == other.gender && size == other.size end def to_s name end end
  4. class Size module WithSize def self.included(model) model.validates :size_group, inclusion: {in:

    Size.groups} model.validates :size_gender, inclusion: {in: Size.genders} model.composed_of :size, mapping: [%w(size_group size_group), %w(size_gender size_gender)], allow_nil: true, constructor: lambda { |group, gender| Size.new(group, gender) } model.extend ClassMethods end module ClassMethods def by_size # TODO: nasty SQL :P end end end end
  5. module UserDeactivator extend self def call(user, active: active) return if

    user.active == active user.update! active: active user.posts.update_all active: active user.votes.update_all active: active user.comments.update_all active: active user.following_association.update_all active: active user.followers_association.update_all active: active end end
  6. module SendPrivateMessage extend self def call(message, recipient, sender) return :invalid

    unless recipient.present? return :invalid unless recipient.receive_private_messages? message = Message.new message: message, sender: sender, recipient: recipient return :not_saved unless message.save MessageMailer.user_message(message).deliver_later Metrics.track_create message, by: sender :success end end
  7. class RegistrationController < ApplicationController def create @user = User.new(user_params) @user.account

    = Account.new(account_params) @user.account.owner = @user if @user.valid? && @user.account.valid? @user.save! @user.account.save! AccountWasCreated.perform_later(account) redirect_to user_path(@user) else render :new end end end
  8. class RegistrationController < ApplicationController def create @registration = RegistrationForm.new(registration_param) if

    @registration.save redirect_to user_path(@registration.user) else render :new end end end
  9. class RegistrationForm include ActiveModel::Model attr_reader :user, :account attr_accessor :first_name, :last_name,

    :email, :name, :plan, :terms_of_service # user validation validates :first_name, presence: true validates :last_name, presence: true validates :email, presence: true, email: true # account validation validates :account_name, presence: true # form custom validation validates :plan, inclusion: {in AccountPlan::VALUES} validates :terms_of_service, acceptance: true # ensure uniqueness validate :ensure_unique_user_email validate :ensure_unique_account_name def initialize @user = User.new @account = Account.new owner: @user end def update(attributes) attributes.each do |name, value| public_send "#{name}=", value end if valid? & user.update(user_attributes) && account.update(account_attributes) account.users << user AccountWasCreated.perform_later(account)
  10. def initialize @user = User.new @account = Account.new owner: @user

    end def update(attributes) attributes.each do |name, value| public_send "#{name}=", value end if valid? & user.update(user_attributes) && account.update(account_attributes) account.users << user AccountWasCreated.perform_later(account) else false end end private def ensure_unique_user_email errors.add :email, 'already taken' if User.where(email: email).any? end def ensure_unique_account_name errors.add :name, 'already taken' if Account.where(name: name).any? end def user_attributes {first_name: first_name, last_name: last_name, email: email} end def account_attributes {plan: plan, name: name} end end
  11. class RegistrationForm include MiniForm::Model model :user, attributes: %i(first_name last_name email),

    save: true model :account, attributes: %i(name plan), save: true attributes :terms_of_service validates :plan, inclusion: {in AccountPlan::VALUES} validates :terms_of_service, acceptance: true def initialize @user = User.new @account = Account.new owner: @user end def perform account.users << user AccountWasCreated.perform_later(account) end end
  12. module ProductRanking def initialize(scope = Product) @scope = Product end

    def all query end def page(page, per_page: 10) query.per_page(per_page).page(page) end def find_each(&block) query.find_each(&block) end private def query # really nasty SQL SELECT end end
  13. class PostSearch include SearchObject.module scope { Post.all } option(:name) {

    |scope, value| scope.where name: value } option(:created_at) { |scope, dates| scope.created_after dates } option(:published, false) { |scope, value| value ? scope.unopened : scope.opened } end
  14. search = PostSearch.new filters: params[:filters] # accessing search options search.name

    # => name option search.created_at # => created at option # accessing results search.count # => number of found results search.results? # => is there any results found search.results # => found results # params for url generations search.params # => option values search.params opened: false # => overwrites the 'opened' option
  15. class ArticlePresenter attr_reader :article delegate :title, :headline, to: :article def

    initialize(article) @article = article end def publication_status if published? "Published at #{published_at}" else "Unpublished" end end def content BodyFormat.call(article.content) end private def published_at article.published_at.strftime("%A, %B %e") end end
  16. class ArticleDecorator attr_reader :article, :view delegate :title, :headline, to: :article

    def initialize(article, view) @article = article @view = view end def publication_status if published? "Published at #{published_at}" else "Unpublished" end end def publication_status_tag @view.content_tag :span, class: 'article-status', publication_status end def headline_tag @view.content_tag :h1, title end def render_comments @view.render 'comments/list', comments: article.comments end private def published_at article.published_at.strftime("%A, %B %e") end end
  17. <%= decorate_article @article do |article| %> <%= article.headline_tag %> <%=

    article.status_tag %> <%= article.cotnent_tag %> <%= article.render_comments %> <% end %>
  18. class ArticleDecorator < Draper::Decorator delegate_all def publication_status if published? "Published

    at #{published_at}" else "Unpublished" end end private def published_at object.published_at.strftime("%A, %B %e") end end
  19. def show @news = News.find params[:id] @comments = @news.comments.page(params[:page]) @ads

    = Advertisement.for(current_user, @news) @related_news = @news.related_news end
  20. class NewsShowPage attr_reader :news def initialize(news, current_user, params) @news =

    news @user = current_user @params = params end def comments @comments ||= news.comments.page(@params[:page]) end def ads @ads ||= Advertisement.for(@user, news) end def related_news @related_news ||= news.related_news end end
  21. module TourHelper def with_tour yield ShowTour.new(self) end class ShowTour def

    initialize(context) @context = context @just_registered = context.session.delete(:show_tour) end def show? @just_registered || @context.cookies[:hide_tour].blank? end def scene @just_registered ? 5 : 1 end def show_hi_user? @just_registered end def max_scene @context.logged_in? 5 : 6 end def link text = show? ? 'Hide tour' : 'Show tour' @context.link_to text, '#', class: 'focus js-toggle-tour' end end end
  22. class User < ActiveRecord::Base # # long list of relationships

    # validates :plan, inclusion: {in AccountPlan::VALUES} validates :terms_of_service, acceptance: true validates :first_name, presence: true validates :last_name, presence: true validates :email, presence: true, email: true, unique: true validates :password, presence: true if: :password_required? validate :old_password_correct, on: :update validates :gender, in: %w(m f u) validates :terms_of_service, acceptance: true, on: :create before_create :record_registration_ip after_create :reset_perishable_token! after_create :send_wellcome_email before_update :send_password_instructions, if: :password_token_changed? before_update :enqueue_avatar_update, if: :image_changed? accepts_nested_attributes_for :hobbies, allow_destroy: true
  23. class << self def ten_most_popular find_by_sql <<-SQL SQL end def

    search(fields) scope = all scope = find_by_name(scope, fields[:name]) if fields[:name] scope = find_by_email(scope, fields[:email]) if fields[:email] # ... scope end private def find_by_name(scope, value) scope.where('first_name LIKE ? OR last_name LIKE ?', value, value) end def find_by_email(scope, email) scope.where(email: email) end end def tshirt_size # ... end
  24. end def tshirt_size # ... end def shipping_address # ...

    end def payment_address # ... end def name "#{first_name} #{last_name}" end def user_status if trashed? 'Deleted user' else if !active? 'Inactive user' else if spammer? 'Spammer' else 'Normal user' end end
  25. end end def activate # ... end def deactivate #

    ... end def require_password_change(password) # ... end private def password_required? # ... end def record_registration_ip # ... end def old_password_correct # ... end def enqueue_avatar_update
  26. def deactivate # ... end def require_password_change(password) # ... end

    private def password_required? # ... end def record_registration_ip # ... end def old_password_correct # ... end def enqueue_avatar_update # ... end end
  27. after_create :reset_perishable_token! after_create :send_wellcome_email before_update :send_password_instructions, if: :password_token_changed? before_update :enqueue_avatar_update,

    if: :image_changed? accepts_nested_attributes_for :hobbies, allow_destroy: true mount_uploader :avatar, Users::AvatarUploader class << self def ten_most_popular find_by_sql <<-SQL SQL end def search(fields) scope = all scope = find_by_name(scope, fields[:name]) if fields[:name] scope = find_by_email(scope, fields[:email]) if fields[:email] # ... scope end private def find_by_name(scope, value)
  28. class << self def search(fields) scope = all scope =

    find_by_name(scope, fields[:name]) if fields[:name] scope = find_by_email(scope, fields[:email]) if fields[:email] # ... scope end private def find_by_name(scope, value) scope.where('first_name LIKE ? OR last_name LIKE ?', value, value) end def find_by_email(scope, email) scope.where(email: email) end end def tshirt_size # ... end def shipping_address # ... end def payment_address # ... end def name "#{first_name} #{last_name}" end
  29. module UserSearch include SearchObject.module scope { User.all } option(:name) {

    |scope, value| scope.where 'first_name LIKE ? OR last_name LIKE ?', option(:email) { |scope, value| scope.where email: value } # ... end
  30. accepts_nested_attributes_for :hobbies, allow_destroy: true mount_uploader :avatar, Users::AvatarUploader def tshirt_size #

    ... end def shipping_address # ... end def payment_address # ... end def name "#{first_name} #{last_name}" end def user_status if trashed? 'Deleted user' else if !active? 'Inactive user' else if spammer? 'Spammer' else 'Normal user' end end def activate # ... end
  31. class UserPresenter < SimpleDelegator def name "#{first_name} #{last_name}" end def

    status if trashed? 'Deleted user' else if !active? 'Inactive user' else if spammer? 'Spammer' else 'Normal user' end end end
  32. before_create :record_registration_ip after_create :reset_perishable_token! after_create :send_wellcome_email before_update :send_password_instructions, if: :password_token_changed?

    before_update :enqueue_avatar_update, if: :image_changed? accepts_nested_attributes_for :hobbies, allow_destroy: true mount_uploader :avatar, Users::AvatarUploader def tshirt_size # ... end def shipping_address # ... end def payment_address # ... end def activate # ... end def deactivate # ... end def require_password_change(password) # ... end
  33. validates :gender, in: %w(m f u) validates :terms_of_service, acceptance: true,

    on: :create before_create :record_registration_ip after_create :reset_perishable_token! after_create :send_wellcome_email before_update :send_password_instructions, if: :password_token_changed? before_update :enqueue_avatar_update, if: :image_changed? accepts_nested_attributes_for :hobbies, allow_destroy: true mount_uploader :avatar, Users::AvatarUploader def tshirt_size # ... end def shipping_address # ... end def payment_address # ... end def require_password_change(password) # ... end private def password_required? # ...
  34. validates :plan, inclusion: {in AccountPlan::VALUES} validates :terms_of_service, acceptance: true validates

    :first_name, presence: true validates :last_name, presence: true validates :email, presence: true, email: true, unique: true validates :terms_of_service, acceptance: true, on: :create validates :password, presence: true if: :password_required? validate :old_password_correct, on: :update before_create :record_registration_ip after_create :reset_perishable_token! after_create :send_wellcome_email before_update :send_password_instructions, if: :password_token_changed? before_update :enqueue_avatar_update, if: :image_changed? accepts_nested_attributes_for :hobbies, allow_destroy: true mount_uploader :avatar, Users::AvatarUploader def require_password_change(password) # ... end private def password_required? # ... end def record_registration_ip # ...
  35. class RegistrationForm include MiniForm::Model model :user, attributes: %i(model attributes), save:

    true attributes :terms_of_service, :password, :password_confirmation validates :terms_of_service, acceptance: true validates :password, presence: true, confirmation: true def initialize @user = User.new end def before_update record_registration_ip end def perform reset_perishable_token! send_wellcome_email end private def record_registration_ip # ... end end
  36. Size.apply_to self, :tshirt_size Address.apply_to self, :payment Address.apply_to self, :shipping validates

    :plan, inclusion: {in AccountPlan::VALUES} validates :first_name, presence: true validates :last_name, presence: true validates :email, presence: true, email: true, unique: true validates :password, presence: true if: :password_required? validate :old_password_correct, on: :update before_update :send_password_instructions, if: :password_token_changed? before_update :enqueue_avatar_update, if: :image_changed? accepts_nested_attributes_for :hobbies, allow_destroy: true mount_uploader :avatar, Users::AvatarUploader def require_password_change(password) # ... end private def password_required? # ... end def old_password_correct # ... end def enqueue_avatar_update
  37. class UpdatePasswordForm include MiniForm::Model attributes :current_password, :password, :password_confirmation validates :password,

    presence: true, confirmation: true validate :ensure_current_password_is_correct attr_reader :user def initialize(user) @user = user end def preform # things ActiveModel::SecurePassword # will take care for proper password storage user.update! password: password end private def ensure_current_password_is_correct # ... end end
  38. class User < ActiveRecord::Base # # long list of relationships

    # Size.apply_to self, :tshirt_size Address.apply_to self, :payment Address.apply_to self, :shipping validates :plan, inclusion: {in AccountPlan::VALUES} validates :first_name, presence: true validates :last_name, presence: true validates :email, presence: true, email: true, unique: true before_update :send_password_instructions, if: :password_token_changed? before_update :enqueue_avatar_update, if: :image_changed? accepts_nested_attributes_for :hobbies, allow_destroy: true mount_uploader :avatar, Users::AvatarUploader def require_password_change(password) # ... end private def enqueue_avatar_update # ... end end
  39. class User < ActiveRecord::Base # # long list of relationships

    # Size.apply_to self, :tshirt_size Address.apply_to self, :payment Address.apply_to self, :shipping validates :plan, inclusion: {in AccountPlan::VALUES} validates :first_name, presence: true validates :last_name, presence: true validates :email, presence: true, email: true, unique: true before_update :send_password_instructions, if: :password_token_changed? before_update :enqueue_avatar_update, if: :image_changed? accepts_nested_attributes_for :hobbies, allow_destroy: true mount_uploader :avatar, Users::AvatarUploader private def enqueue_avatar_update # ... end end
  40. class User < ActiveRecord::Base # # long list of relationships

    # Size.apply_to self, :tshirt_size Address.apply_to self, :payment Address.apply_to self, :shipping validates :plan, inclusion: {in AccountPlan::VALUES} validates :terms_of_service, acceptance: true validates :first_name, presence: true validates :last_name, presence: true validates :email, presence: true, email: true, unique: true before_update :enqueue_avatar_update, if: :image_changed? mount_uploader :avatar, Users::AvatarUploader private def enqueue_avatar_update # ... end end
  41. class User < ActiveRecord::Base # # long list of relationships

    # Size.apply_to self, :tshirt_size Address.apply_to self, :payment Address.apply_to self, :shipping validates :plan, inclusion: {in AccountPlan::VALUES} validates :first_name, presence: true validates :last_name, presence: true validates :email, presence: true, email: true, unique: true end
  42. • Value objects • Service objects • Form objects •

    Query objects • Presenter/Decorator objects • Page object • Helper objects