Usar un alcance de forma predeterminada en una relación has_many de Rails

81

Digamos que tengo las siguientes clases

class SolarSystem < ActiveRecord::Base
  has_many :planets
end

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

Planettiene un alcance life_supportingy SolarSystem has_many :planets. Me gustaría definir mi relación has_many para que cuando pregunte solar_systempor todos los asociados planets, el life_supportingalcance se aplique automáticamente. Esencialmente, me gustaría solar_system.planets == solar_system.planets.life_supporting.

Requisitos

  • Yo no quiero cambiar scope :life_supportingde Planeta

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

  • También me gustaría evitar la duplicación al no tener que agregar a SolarSystem

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

Objetivo

Me gustaria tener algo como

has_many :planets, :with_scope => :life_supporting

Editar: Work Arounds

Como dijo @phoet, puede que no sea posible lograr un alcance predeterminado usando ActiveRecord. Sin embargo, he encontrado dos posibles soluciones. Ambos evitan la duplicación. El primero, aunque largo, mantiene una legibilidad y transparencia obvias, y el segundo es un método de tipo auxiliar cuya salida es explícita.

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

Otra solución que es mucho más limpia es simplemente agregar el siguiente método a SolarSystem

def life_supporting_planets
  planets.life_supporting
end

y para usar solar_system.life_supporting_planetsdonde quiera que lo use solar_system.planets.

Ninguno de los dos responde a la pregunta, así que los pongo aquí como solución alternativa en caso de que alguien más se encuentre con esta situación.

Aaron
fuente
2
su solución con where_vales es realmente la mejor solución disponible y vale la pena una respuesta aceptada
Viktor Trón
where_valuespodría no funcionar con condiciones hash: {:cleared => false}... da una matriz de hash que a ActiveRecord no le gusta. Como truco, tomar el primer elemento de la matriz funciona: Planet.life_supporting.where_values[0]...
Nolan Amy
Descubrí que tenía que usar en where_astlugar de where_valueso where_values_hashcomo había usado AREL en el alcance del otro modelo. ¡Funcionó de maravilla! +1
br3nt

Respuestas:

130

En Rails 4, Associationstenga un scopeparámetro opcional que acepte una lambda que se aplica al Relation(consulte el documento de 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

En Rails 3, la where_valuessolución a veces se puede mejorar utilizando where_values_hashque maneja mejores ámbitos donde las condiciones están definidas por múltiples whereo por un hash (no es el caso aquí).

has_many :planets, conditions: Planet.life_supporting.where_values_hash
user1003545
fuente
¿Podría detallar la solución rails 3? ¡Los rieles 4 one están tan limpios!
Augustin Riedinger
1
Para Rails 3, creo que debería leerse has_many :planets, conditions: Planet.life_supporting.where_values_hashpara hacer cumplir el alcance. Esto también es dorado para una carga ansiosa.
nerfólogo
1
Descubrí por las malas que where_values_hashno funciona con texto donde las cláusulas, por ejemplo User.where(name: 'joe').where_values_hash, devolverán el hash de las condiciones esperadas, mientras User.where('name = ?', 'Joe').where_values_hashque no. Por lo tanto, el ejemplo de los planetas probablemente fracasará.
nerfólogo
1
@nerfólogo Gracias por señalar el error en el último ejemplo de código, edité la respuesta. Tu segundo comentario tiene sentido, creo que es a lo que me refería en el último párrafo de la respuesta. Siéntase libre de editar mi respuesta si puede encontrar una forma más clara de explicar las limitaciones.
user1003545
2
@ GrégoireClermont esto ya no funciona en Rails 5
elquimista
16

En Rails 5, el siguiente código funciona bien ...

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

  class Store 
    has_many :paid_orders, -> { paid }, class_name: 'Order'
  end 
Martin Streicher
fuente
1

Acabo de profundizar en ActiveRecord y no parece que esto se pueda lograr con la implementación actual de has_many. puede pasar un bloque a, :conditionspero esto se limita a devolver un hash de condiciones, no cualquier tipo de cosas arel.

una forma realmente simple y transparente de lograr lo que desea (lo que creo que está tratando de hacer) es aplicar el alcance en tiempo de ejecución:

  # foo.rb
  def bars
    super.baz
  end

esto está lejos de lo que está pidiendo, pero podría funcionar;)

foet
fuente
¡Gracias phoet! Esto va a trabajar, sin embargo, sólo se verá un poco extraño en la revisión de código. Creo que la retroalimentación que obtendré al implementar esto es hacer la duplicación en la has_manydeclaración, ya que está más clara.
Aaron
desde mi punto de revisión de código, preferiría esta opción sobre la duplicación de condiciones, etc., siempre que proporcione una prueba de lo que debe hacer, este enfoque es mucho más DRY y SRP
phoet
Recomiendo encarecidamente no utilizar un método como este en el que normalmente se utilizaría una asociación. En su lugar, eliminaría las condiciones de la asociación y un alcance que se llama explícitamente a la asociación. Será más fácil de mantener y más claro en el futuro.
BM5k
Vaya, acabo de darme cuenta de que esta publicación era muy antigua. Llegué aquí investigando un tema similar.
BM5k