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

要件

  • 私はないではない変更するscope :life_supportingにPlanetします

    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.