Railsのhas_many関係でデフォルトでスコープを使用する


81

次のクラスがあるとしましょう

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スコープlife_supportingSolarSystem has_many :planets。にsolar_system関連付けられているすべてのを要求するとplanetslife_supportingスコープが自動的に適用されるように、has_many関係を定義したいと思います。基本的に、私は欲しいですsolar_system.planets == solar_system.planets.life_supporting

要件

  • 私はないではない変更するscope :life_supportingPlanetします

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

  • また、追加する必要がないので重複を防ぎたい SolarSystem

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

ゴール

のようなものが欲しいのですが

has_many :planets, :with_scope => :life_supporting

編集:回避策

@phoetが言ったように、ActiveRecordを使用してデフォルトのスコープを達成することは不可能かもしれません。ただし、2つの潜在的な回避策を見つけました。どちらも重複を防ぎます。1つ目は、長い間、明らかな可読性と透明性を維持し、2つ目は、出力が明示的なヘルパータイプのメソッドです。

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.


2
your workaround using where_vales is really the best available solution and worth an accepted answer
Viktor Trón

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

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

回答:


130

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

Could you detail the rails 3 solution? The rails 4 one is so clean!
Augustin Riedinger

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

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

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

2
@GrégoireClermont this is no more working in Rails 5
elquimista

16

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 

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


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

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

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

Oops, just realized this post was really old. Got here researching a similar issue.
BM5k
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.