More Generic Scopes

This is a followup to my previous post How to share code among ActiveRecord models. In this installment I’m going to show some additional generic scopes that I found useful for building up queries. In particular, some of these scopes are twice as generic, because they are defined by wrappers, so that their real name is specific enough to be meaningful.

Field Scope

This scope sets a field to some value. Think about a state field. Instead of writing

where(:state, :accepted)

you can write

with_state(:accepted)

which is a bit easier to read.

Here it is.

    # field_scope :state
    #   --> named_scope :with_state, ...
    def field_scope(field, cast = :to_s)
      named_scope :"with_#{field}", lambda { |*args|
        where(field => (args.size == 1 ? args.first.send(cast) : args.map(&cast)))
      }
    end

Example:

class Order
  field_scope :state
  named_scope :not_paid, with_state(:to_be_paid, :paying)
end

Order.not_paid.created_before(Date.yesterday).delete

What I like about this generic scope is that a common word like state becomes a specific term that I can later easily find with a simple and fast text search. So, instead of searching for state, whose matches could include a lot of false positives, I can search for with_state which is specific enough to only match what I’m really looking for. Anyway this is a dull surrogate of what future IDEs (able to understand semantics) will allow, like search for specific usages of common words.

Belonging-To Scope

This scope works analogously to the previous one, but for belongs_to relations. Think about a user_id field. Instead of writing

where(:user_id => tom.id)

you can write

belonging_to(tom)

which is a bit easier to read.

It’s almost always possible to guess the name of the field from the class of the object a model is supposed to belong to. If that’s not the case, I’ve provided an as option that accepts the name of the field (without the _id suffix).

      model.named_scope :belonging_to, lambda { |obj, options = {}|
        options = {
            :as => (obj.nil? || obj.is_a?(Integer)) ? 'user' : obj.class.to_s.underscore
        }.merge(options || {})
        where(:"#{options[:as]}_id" => (obj.respond_to?(:id) ? obj.id : obj))
      }

Example:

class Document
  belongs_to :user
  belongs_to :editor, :class_name => 'User'
end

owned_by_ann = Document.belonging_to(User.with_name('Ann'))
edited_by_me = Document.belonging_to(current_user, :as => :editor)

Polymorphic Scope

This scope works analogously to the previous one, but for belongs_to relations that are also polymorphic. In this case, I decided to retain simplicity by means of the with_ prefix. Think about a liked field that has been defined like this

belongs_to :liked, :polymorphic => true

Then it’s a bit tedious (and rude) to explicitly refer to liked_table and liked_id in queries, so this is how to elegantly magic them away.

    def sti_member?
      column_names.include?(inheritance_column)
    end

    def sti_root?
      return nil unless sti_member?
      superclass == ActiveRecord::Base
    end

    def sti_root
      return nil unless sti_member?
      klass = self
      while ! klass.sti_root?
        klass = klass.superclass
      end
      klass
    end

    # polymorphic_scope :favorite
    #   --> named_scope :with_favorite, ...
    def polymorphic_scope(field)
      named_scope :"with_#{field}", lambda { |obj|
        if obj.is_a?(Class)
          where(:"#{field}_type" => obj.to_s)
        else
          where(:"#{field}_type" => (obj.class.sti_root || obj.class).to_s, :"#{field}_id" => obj.id)
        end
      }
    end

All the sti_* methods account for Single Table Inheritance, which is a little tricky to deal with because of how it works behind the scenes.

Example:

class Like
  belongs_to :liked, :polymorphic => true
  polymorphic_scope :liked
end

class User
  has_many :likes
end

favorite_singers = current_user.likes.with_liked(Artist)
favorite_people = current_user.likes.with_liked(User)
favorite_songs = current_user.likes.with_liked(Song)

liked_lady_gaga = current_user.likes.with_liked(Singer.with_name('Lady_Gaga'))
puts "You like Lady Gaga since #{liked_lady_gaga.created_at}" unless liked_lady_gaga.nil?

Note how natural it all becomes, working equally well for classes (also for STI classes, like Artist < User) and objects (like Lady Gaga).

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.