Verwenden Sie standardmäßig einen Bereich für eine Rails-Beziehung has_many

81

Angenommen, ich habe die folgenden Klassen

class SolarSystem < ActiveRecord::Base
  has_many :planets
end

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

Planethat einen Umfang life_supportingund SolarSystem has_many :planets. Ich möchte meine has_many-Beziehung so definieren, dass der Bereich automatisch angewendet wird , wenn ich solar_systemnach allen zugeordneten Elementen frage . Im Wesentlichen möchte ich .planetslife_supportingsolar_system.planets == solar_system.planets.life_supporting

Bedarf

  • Ich nicht ändern wollen scope :life_supportingin Planetzu

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

  • Ich möchte auch Doppelarbeit verhindern, indem ich nichts hinzufügen muss SolarSystem

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

Tor

Ich hätte gerne so etwas

has_many :planets, :with_scope => :life_supporting

Bearbeiten: Work Around

Wie @phoet sagte, ist es möglicherweise nicht möglich, mit ActiveRecord einen Standardbereich zu erreichen. Ich habe jedoch zwei mögliche Problemumgehungen gefunden. Beides verhindert Doppelarbeit. Die erste Methode ist zwar lang, bietet jedoch eine offensichtliche Lesbarkeit und Transparenz, und die zweite Methode ist eine Hilfsmethode, deren Ausgabe explizit ist.

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

Eine andere Lösung, die viel sauberer ist, besteht darin, einfach die folgende Methode hinzuzufügen SolarSystem

def life_supporting_planets
  planets.life_supporting
end

und solar_system.life_supporting_planetsüberall dort zu verwenden, wo Sie es verwenden möchten solar_system.planets.

Keiner von beiden beantwortet die Frage, also habe ich sie hier nur als Workarounds platziert, sollte jemand anderes auf diese Situation stoßen.

Aaron
quelle
2
Ihre Problemumgehung mit where_vales ist wirklich die beste verfügbare Lösung und eine akzeptierte Antwort wert
Viktor Trón
where_valuesfunktioniert möglicherweise nicht mit Hash-Bedingungen: {:cleared => false}... es gibt eine Reihe von Hashes, die ActiveRecord nicht mag. Als Hack funktioniert das Ergreifen des ersten Elements im Array: Planet.life_supporting.where_values[0]...
Nolan Amy
Ich stellte fest, dass ich AREL im Bereich des anderen Modells where_astanstelle von where_valuesoder where_values_hashwie verwendet verwenden musste. Arbeitete ein Vergnügen! +1
br3nt

Antworten:

130

Verfügen Sie in Rails 4 Associationsüber einen optionalen scopeParameter, der ein Lambda akzeptiert, das auf das angewendet wird Relation(siehe Dokument für 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 kann die where_valuesProblemumgehung manchmal verbessert werden, indem where_values_hashbessere Bereiche verwendet werden, in denen Bedingungen durch mehrere whereoder durch einen Hash definiert sind (hier nicht der Fall).

has_many :planets, conditions: Planet.life_supporting.where_values_hash
user1003545
quelle
Könnten Sie die Rails 3-Lösung detaillieren? Die Schienen 4 ist so sauber!
Augustin Riedinger
1
Für Rails 3 sollte es lauten has_many :planets, conditions: Planet.life_supporting.where_values_hash, um den Geltungsbereich durchzusetzen. Dies ist auch golden für eifriges Laden.
Nerfologe
1
Ich habe herausgefunden, wie schwierig es ist, where_values_hashmit Text nicht zu funktionieren, wenn Klauseln, z. B. User.where(name: 'joe').where_values_hashden Hash der erwarteten Bedingungen zurückgeben, während dies nicht der Fall ist User.where('name = ?', 'Joe').where_values_hash. Daher wird das Planetenbeispiel wahrscheinlich fehlschlagen.
Nerfologe
1
@nerfologist Vielen Dank, dass Sie auf den Fehler im letzten Codebeispiel hingewiesen haben. Ich habe die Antwort bearbeitet. Ihr zweiter Kommentar macht Sinn, ich denke, darauf habe ich im letzten Absatz der Antwort angespielt. Fühlen Sie sich frei, meine Antwort zu bearbeiten, wenn Sie einen klareren Weg finden, um die Einschränkungen zu erklären.
user1003545
2
@ GrégoireClermont dies funktioniert nicht mehr in Rails 5
elquimista
16

In Rails 5 funktioniert der folgende Code einwandfrei ...

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

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

Ich hatte gerade einen tiefen Einblick in ActiveRecord und es sieht nicht so aus, als ob dies mit der aktuellen Implementierung von erreicht werden kann has_many. Sie können einen Block an übergeben :conditions, dies beschränkt sich jedoch darauf, einen Hash von Bedingungen zurückzugeben, nicht irgendeine Art von Arel-Zeug.

Ein wirklich einfacher und transparenter Weg, um das zu erreichen, was Sie wollen (was ich denke, dass Sie versuchen zu tun), besteht darin, den Bereich zur Laufzeit anzuwenden:

  # foo.rb
  def bars
    super.baz
  end

das ist weit von dem entfernt, wonach du fragst, aber es könnte einfach funktionieren;)

Phoet
quelle
Danke phoet! Dies wird funktionieren, sieht jedoch in der Codeüberprüfung nur ein bisschen seltsam aus. Ich denke, das Feedback, das ich bei der Implementierung erhalten werde, besteht darin, stattdessen die Duplizierung der has_manyErklärung vorzunehmen, da dies klarer ist.
Aaron
Aus meiner Sicht der Codeüberprüfung würde ich diese Option der Verdoppelung von Bedingungen usw. vorziehen. Solange Sie einen Test für die beabsichtigten
Aufgaben bereitstellen
Ich empfehle dringend, keine solche Methode zu verwenden, bei der normalerweise eine Zuordnung verwendet wird. Ich würde stattdessen die Bedingungen aus der Assoziation entfernen und einen Bereich, der explizit für die Assoziation aufgerufen wird. Es wird in Zukunft wartbarer und klarer sein.
BM5k
Hoppla, ich habe gerade festgestellt, dass dieser Beitrag wirklich alt ist. Ich bin hierher gekommen, um ein ähnliches Problem zu untersuchen.
BM5k