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


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) :


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


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 =>

you can write


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))


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

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?

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

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

    # 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)
          where(:"#{field}_type" => (obj.class.sti_root || obj.class).to_s, :"#{field}_id" =>

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.


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

class User
  has_many :likes

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.