93

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.

Community
  • 1
  • 1
Aaron
  • 13,349
  • 11
  • 66
  • 105
  • 2
    your workaround using where_vales is really the best available solution and worth an accepted answer – Viktor Trón Mar 21 '13 at 16:52
  • `where_values` might not work with hash conditions: `{:cleared => false}` ... it gives an array of hashes that ActiveRecord doesn't like. As a hack, grabbing the first item in the array works: `Planet.life_supporting.where_values[0]`... – Nolan Amy Oct 20 '13 at 00:32
  • I found I had to use `where_ast` rather than `where_values` or `where_values_hash` as I had used AREL in the scope on the other model. Worked a treat! +1 – br3nt Jun 11 '15 at 23:07

3 Answers3

147

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
user1003545
  • 2,078
  • 1
  • 15
  • 15
  • Could you detail the rails 3 solution? The rails 4 one is so clean! – Augustin Riedinger Jan 27 '14 at 14:31
  • Looks like this was wanted by enough people that it was added to Rails 4 :D – Aaron Jan 27 '14 at 16:42
  • @AugustinRiedinger The "Edit: Work Arounds" part of the question describes it quite well. The improvement I suggest adresses the problem with hash conditions described by Nolan in the question comments. – user1003545 Jan 27 '14 at 17:34
  • @Aaron yes I think it really makes sense to me (eg. group members and group admins who are member with scope admin). @gregoire I actually made it work with `where_values_hash`. But I wish the Rails 4 solution could be imported into Rails 3. Anyway thanks! – Augustin Riedinger Jan 27 '14 at 18:11
  • 1
    For Rails 3, I take it should read ```has_many :planets, conditions: Planet.life_supporting.where_values_hash``` to enforce the scope. This is also golden for eager loading. – nerfologist Jan 30 '15 at 17:21
  • 1
    I found out the hard way that ```where_values_hash``` does not work with text **where** clauses, e.g. ```User.where(name: 'joe').where_values_hash``` will return the expected conditions' hash, whereas ```User.where('name = ?', 'Joe').where_values_hash``` will not. Therefore, the planets example will likely fail. – nerfologist Jan 30 '15 at 17:38
  • 1
    @nerfologist Thanks for pointing out the mistake in the last code example, I edited the answer. Your second comment makes sense, I think it's what I was alluding to in the last paragraph of the answer. Feel free to edit my answer if you can find a clearer way to explain the limitations. – user1003545 Jan 31 '15 at 14:09
  • 2
    @GrégoireClermont this is no more working in Rails 5 – elquimista Oct 02 '17 at 06:27
26

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 
Martin Streicher
  • 1,983
  • 1
  • 18
  • 18
1

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

phoet
  • 18,688
  • 4
  • 46
  • 74
  • Thanks phoet! This **will** work, however it will just look a bit odd in the code review. I think the feedback I'll get when implementing this is to instead do the duplication on the `has_many` declaration as it's clearer. – Aaron Jul 25 '12 at 02:34
  • from my point of code-review i would prefer this option over duplication of conditions etc. as long as you provide a test for what it's meant to do, this approach is much more DRY and SRP – phoet Jul 25 '12 at 06:48
  • I highly recommend against using a method like this where an association would normally be used. I would instead remove the conditions from the association & a scope that is explicitly called on the association. It will be more maintainable and more clear in the future. – BM5k Apr 13 '13 at 05:23
  • Oops, just realized this post was *really* old. Got here researching a similar issue. – BM5k Apr 13 '13 at 05:24