Use a scope by default on a Rails has_many relationship

Ruby on-Rails-3ActiverecordHas Many

Ruby on-Rails-3 Problem Overview


Let's say I have the following classes

class SolarSystem < ActiveRecord::Base
  has_many :planets
end

class Planet < ActiveRecord::Base
  scope :life_supporting, where('distance_from_sun > ?', 5).order('diameter ASC')
end

Planet has a scope life_supporting and SolarSystem has_many :planets. I would like to define my has_many relationship so that when I ask a solar_system for all associated planets, the life_supporting scope is automatically applied. Essentially, I would like solar_system.planets == solar_system.planets.life_supporting.

Requirements

  • I do not want to change scope :life_supporting in Planet to

    default_scope where('distance_from_sun > ?', 5).order('diameter ASC')

  • I'd also like to prevent duplication by not having to add to SolarSystem

    has_many :planets, :conditions => ['distance_from_sun > ?', 5], :order => 'diameter ASC'

Goal

I'd like to have something like

has_many :planets, :with_scope => :life_supporting

Edit: Work Arounds

As @phoet said, it may not be possible to achieve a default scope using ActiveRecord. However, I have found two potential work arounds. Both prevent duplication. The first one, while long, maintains obvious readability and transparency, and the second one is a helper type method who's output is explicit.

class SolarSystem < ActiveRecord::Base
  has_many :planets, :conditions => Planet.life_supporting.where_values,
    :order => Planet.life_supporting.order_values
end

class Planet < ActiveRecord::Base
  scope :life_supporting, where('distance_from_sun > ?', 5).order('diameter ASC')
end

Another solution which is a lot cleaner is to simply add the following method to SolarSystem

def life_supporting_planets
  planets.life_supporting
end

and to use solar_system.life_supporting_planets wherever you'd use solar_system.planets.

Neither answers the question so I just put them here as work arounds should anyone else encounter this situation.

Ruby on-Rails-3 Solutions


Solution 1 - Ruby on-Rails-3

In Rails 4, Associations have an optional scope parameter that accepts a lambda that is applied to the Relation (cf. the doc for ActiveRecord::Associations::ClassMethods)

class SolarSystem < ActiveRecord::Base
  has_many :planets, -> { life_supporting }
end

class Planet < ActiveRecord::Base
  scope :life_supporting, -> { where('distance_from_sun > ?', 5).order('diameter ASC') }
end

In Rails 3, the where_values workaround can sometimes be improved by using where_values_hash that handles better scopes where conditions are defined by multiple where or by a hash (not the case here).

has_many :planets, conditions: Planet.life_supporting.where_values_hash

Solution 2 - Ruby on-Rails-3

In Rails 5, the following code works fine...

  class Order 
    scope :paid, -> { where status: %w[paid refunded] }
  end 

  class Store 
    has_many :paid_orders, -> { paid }, class_name: 'Order'
  end 

Solution 3 - Ruby on-Rails-3

i just had a deep dive into ActiveRecord and it does not look like if this can be achieved with the current implementation of has_many. you can pass a block to :conditions but this is limited to returning a hash of conditions, not any kind of arel stuff.

a really simple and transparent way to achieve what you want (what i think you are trying to do) is to apply the scope at runtime:

  # foo.rb
  def bars
    super.baz
  end

this is far from what you are asking for, but it might just work ;)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAaronView Question on Stackoverflow
Solution 1 - Ruby on-Rails-3user1003545View Answer on Stackoverflow
Solution 2 - Ruby on-Rails-3Martin StreicherView Answer on Stackoverflow
Solution 3 - Ruby on-Rails-3phoetView Answer on Stackoverflow