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

Starving ActiveRecord

Starving ActiveRecord

Some techniques to deal with fat ActiveRecord models.

Avatar for Radoslav Stankov

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