Domyślnie użyj zasięgu w relacji has_many w Railsach

81

Powiedzmy, że mam następujące klasy

class SolarSystem < ActiveRecord::Base
  has_many :planets
end

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

Planetma zakres life_supportingi SolarSystem has_many :planets. Chciałbym zdefiniować moją relację has_many, aby gdy pytam solar_systemo wszystkie powiązane planets, life_supportingzakres jest stosowany automatycznie. Zasadniczo chciałbym solar_system.planets == solar_system.planets.life_supporting.

Wymagania

  • Ja nie chce zmienić scope :life_supportingw Planetcelu

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

  • Chciałbym również zapobiec powielaniu, ponieważ nie muszę nic dodawać SolarSystem

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

Cel

Chciałbym mieć coś takiego

has_many :planets, :with_scope => :life_supporting

Edycja: praca wokół

Jak powiedział @phoet, osiągnięcie domyślnego zakresu przy użyciu ActiveRecord może nie być możliwe. Jednak znalazłem dwa potencjalne obejścia. Oba zapobiegają powielaniu. Pierwsza, choć długa, zachowuje oczywistą czytelność i przejrzystość, a druga jest metodą pomocniczą, której wynik jest wyraźny.

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

Innym rozwiązaniem, które jest dużo czystsze, jest po prostu dodanie następującej metody do SolarSystem

def life_supporting_planets
  planets.life_supporting
end

i używać solar_system.life_supporting_planetswszędzie tam, gdzie chceszsolar_system.planets .

Żadne z nich nie odpowiada na pytanie, więc po prostu umieszczam je tutaj, aby obejść tę sytuację, gdyby ktoś inny napotkał taką sytuację.

Aaron
źródło
2
Twoje obejście przy użyciu where_vales jest naprawdę najlepszym dostępnym rozwiązaniem i wartym zaakceptowanej odpowiedzi
Viktor Trón
where_valuesmoże nie działać z warunkami mieszania: {:cleared => false}... daje tablicę skrótów, których ActiveRecord nie lubi. Jako hack, przechwycenie pierwszego elementu w tablicy działa: Planet.life_supporting.where_values[0]...
Nolan Amy
Okazało się, że musiałem użyć where_astzamiast where_valueslub where_values_hashtak, jak użyłem AREL w lunecie w innym modelu. Udało się! +1
br3nt

Odpowiedzi:

130

W Railsach 4 Associationsmasz opcjonalny scopeparametr, który akceptuje lambdę, która jest stosowana do Relation(por. Dokumentacja 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

W Railsach 3 where_valuesobejście można czasami poprawić, używając where_values_hashtego, który obsługuje lepsze zakresy, w których warunki są definiowane przez wielokrotność wherelub przez hash (nie ma to miejsca w tym przypadku).

has_many :planets, conditions: Planet.life_supporting.where_values_hash
user1003545
źródło
Czy mógłbyś szczegółowo opisać rozwiązanie Rails 3? Szyny 4 jeden są takie czyste!
Augustin Riedinger
1
W przypadku Rails 3 powinienem przeczytać, has_many :planets, conditions: Planet.life_supporting.where_values_hashaby wymusić zakres. Jest to również złote dla chętnego ładowania.
nerfolog
1
Dowiedziałem się na własnej skórze, że where_values_hashnie działa to z tekstem, w którym klauzule, np. User.where(name: 'joe').where_values_hashZwrócą hash oczekiwanych warunków, podczas gdy User.where('name = ?', 'Joe').where_values_hashnie. Dlatego przykład planet prawdopodobnie zawiedzie.
nerfolog
1
@nerfologist Dzięki za wskazanie błędu w ostatnim przykładzie kodu, zredagowałem odpowiedź. Twój drugi komentarz ma sens, myślę, że nawiązałem do niego w ostatnim akapicie odpowiedzi. Zapraszam do edycji mojej odpowiedzi, jeśli możesz znaleźć jaśniejszy sposób na wyjaśnienie ograniczeń.
user1003545
2
@ GrégoireClermont to już nie działa w Railsach 5
elquimista
16

W Railsach 5 następujący kod działa dobrze ...

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

  class Store 
    has_many :paid_orders, -> { paid }, class_name: 'Order'
  end 
Martin Streicher
źródło
1

Właśnie zagłębiłem się w ActiveRecord i nie wygląda na to, że można to osiągnąć przy obecnej implementacji has_many. możesz przejść blok do:conditions ale jest to ograniczone do zwrócenia skrótu warunków, a nie jakichkolwiek rzeczy związanych z arelami.

naprawdę prostym i przejrzystym sposobem osiągnięcia tego, co chcesz (co myślę, że próbujesz zrobić) jest zastosowanie zakresu w czasie wykonywania:

  # foo.rb
  def bars
    super.baz
  end

to jest dalekie od tego, o co prosisz, ale może po prostu zadziałać;)

phoet
źródło
Dzięki phoet! To będzie działać, jednak będzie to tylko wyglądać nieco dziwnie w przeglądzie kodu. Myślę, że informacje zwrotne, które otrzymam po wdrożeniu tego, polegają na tym, aby zamiast tego wykonać powielenie has_manydeklaracji, ponieważ jest to wyraźniejsze.
Aaron,
z mojego punktu przeglądu kodu wolałbym tę opcję niż powielanie warunków itp., o ile zapewniasz test na to, co ma zrobić, to podejście jest znacznie bardziej SUCHE i SRP
phoet
Zdecydowanie odradzam stosowanie takiej metody, w której normalnie byłoby używane skojarzenie. Zamiast tego usunąłbym warunki ze skojarzenia i zakresu, który jest wyraźnie wywoływany w skojarzeniu. W przyszłości będzie łatwiejszy w utrzymaniu i bardziej przejrzysty.
BM5k
Ups, właśnie sobie uświadomiłem, że ten post jest naprawdę stary. Dotarłem tutaj, aby zbadać podobny problem.
BM5k