Running with Ruby

Tag: ActiveRecord (page 2 of 9)

ActiveRecord isolated, namespaced scopes and methods using instance_exec and ActiveSupport::Concern

Rails concerns are a really good way to provide shared functionalities into your models. For example you can add scopes and then apply them in multiple classes:

module Statistics
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }
  end
end

class Host
  include Statistics
end

One of the downsides of this solution is a “models pollution”. Every model in which we want to include our module, will get all the methods and scopes from that module. With multiple concerns and extensions, it can create issues with methods and scopes overwriting each other. But as always with Ruby, there’s a quite nice solution that will allow you to define custom scopes that will be encapsulated in a namespace method. This approach allows to have multiple scopes with the same name that will vary depending on an invocation context:

module Statistics
  extend ActiveSupport::Concern

  included do
    # Some magic here...
    scope :last_week, -> { where('created_at >= ?', 1.week.ago) }
  end
end

module Calculable
  extend ActiveSupport::Concern

  included do
    # Some magic here...
    scope :last_week, -> { unscoped.where('created_at >= ?', 1.week.ago) }
  end
end

class Host
  include Statistics
  include Calculable
end

# last_week accessible via statistics namespace
Host.statistics.last_week #=> all the hosts created last week
# last_week not available directly on a host class
Host.last_week #=> This will raise NoMethodError: undefined method last_week
# last_week should be also chainable with other scopes
Host.enabled.statistics.last_week #=> all enabled hosts from last week
# And the last_week method but from the calculable namespace
Host.calculable.last_week

Proxy object and instance_exec as a solution

To obtain such a behavior we can use a proxy object in our concern module:

module Statisticable
  extend ActiveSupport::Concern

  # Class that acts as a simple proxy for all statistical scopes
  # We have it, because we don't want to "pollute" original klass
  # that we extend with this. This way we can define scopes here
  # and then access them via .statistics class method of a class
  class Proxy
    # @param [Symbol] name of a scope or a method
    # @param [Block] block with a scope that will be binded to the class
    def self.scope(name, block)
      define_method(name) do |*args|
        @scope.instance_exec(*args, &block)
      end
    end

    # @param scope [ActiveRecord::Base] active record class for which we want to
    #   create statistics proxy
    def initialize(scope)
      @scope = scope
      # Here we could also use any scope that we want, like:
      # @scope = scope.unscoped
    end

    %i( week month year ).each do |time|
      # @return [Integer] number of elements that were created in a certain time
      scope :"last_#{time}", -> { where('created_at >= ?', 1.send(time).ago) }
    end

    # We can add any standard method as well - but it can't be chained further
    scope :summed, -> { sum(:id) }
  end

  included do
    # @return [Statisticable::Proxy] proxy that provides us with scopes for statistical info
    #   about a given class
    # @example Get statistics proxy for Like class
    #   Like.statistics #=> Statisticable::Proxy instance
    def self.statistics
      @statistics ||= Statisticable::Proxy.new(self)
    end
  end
end

It uses the instance_exec method to execute all the scopes in our base class (@scope) context.

Now in every AR in which we include this Statisticable concern, all its scopes and methods will be available via statistics namespace.

Extending ActiveRecord association to cleanup your associations code

For some reason not many RoR developers know that they can extend ActiveRecord associations. This feature can be a great way to cleanup any relational code that is dependent on a parent resource (or to just add a simple functionalities to scopes). Here’s a really simple example how you can use it:

class User < ActiveRecord::Base
  module GroupActions
    # Here you work on a scope
    def reactivate!
      # I know we could use update_all - this is not the case ;)
      map(&:reactivate!)
    end
  end

  belongs_to :group

  def reactivate!
    update!(active: true)
  end
end

class Group < ActiveRecord::Base
  has_many :users, extend: User::GroupActions
end

That way you can perform actions like this one:

# Looks much better than any method like current_group.reactivate_users!
current_group.users.reactivate!

Dynamic pagination (changeable per page on a parent resource)

You can also use a small trick to access parent model and it’s data. It can be useful for example when implementing a dynamic pagination model (based on a parent key value) or any other functionality that somehow depends on a relation owning model. Instead of doing something like this in your controllers:

def index
  @pictures = current_gallery.pictures.per(current_gallery.per_page).page(current_page)
end

you can leave the implementation details out of it (which in general is a good idea):

def index
  @pictures = current_gallery.pictures.paginated(current_page)
end

And this is how you can achieve such a behavior:

class Gallery < ActiveRecord::Base
  has_many :pictures,
    extend: Picture::RelationExtensions

  validates :per_page,
    numericality: { only_integer: true },
    inclusion: { in: (1..100).to_a }
end

class Picture < ActiveRecord::Base
  module RelationExtensions
    def paginated(current_page)
      # We create a picture instance that has a gallery reference already
      # because in the controller - gallery is a current scope starting point
      page(current_page).per(self.new.gallery.per_page)
    end
  end

  belongs_to :gallery, inverse_of:  :pictures
end

It’s a really great idea to use approach like this also because you can use it with any standard scope:

current_gallery.active.paginated(current_page)
# or if you return an active record association you can chain methods
current_gallery.active.paginated(current_page).reactivate!

Conclusion

If you want to hide implementation details of any association related action (especially when it uses owning model fields data), using ActiveRecord association can be a really good idea.

Olderposts Newerposts

Copyright © 2017 Running with Ruby

Theme by Anders NorenUp ↑