2016-12-13 10 views
11

Niewiele odkryłem, w jaki sposób można pisać zakresy powiązań polimorficznych w szynach, nie mówiąc już o tym, jak pisać zapytania na stowarzyszeniach polimorficznych.Zakresy i zapytania ze stowarzyszeniami polimorficznymi

W dokumentacji Railsa przyjrzałem się Polymorphic Associations section, Joining Tables section i Scopes section. Zrobiłem też mój uczciwy udział w wyszukiwaniu.

Weź tę konfigurację na przykład:

class Pet < ActiveRecord::Base 
    belongs_to :animal, polymorphic: true 
end 

class Dog < ActiveRecord::Base 
    has_many :pets, as: :animal 
end 

class Cat < ActiveRecord::Base 
    has_many :pets, as: :animal 
end 

class Bird < ActiveRecord::Base 
    has_many :pets, as: :animal 
end 

Więc Pet może być animal_type "Dog", "kot" lub "ptak".

Aby pokazać wszystkie struktury tabeli: tu jest mój schema.rb:

create_table "birds", force: :cascade do |t| 
    t.datetime "created_at", null: false 
    t.datetime "updated_at", null: false 
end 

create_table "cats", force: :cascade do |t| 
    t.integer "killed_mice" 
    t.datetime "created_at", null: false 
    t.datetime "updated_at", null: false 
end 

create_table "dogs", force: :cascade do |t| 
    t.boolean "sits" 
    t.datetime "created_at", null: false 
    t.datetime "updated_at", null: false 
end 

create_table "pets", force: :cascade do |t| 
    t.string "name" 
    t.integer "animal_id" 
    t.string "animal_type" 
    t.datetime "created_at", null: false 
    t.datetime "updated_at", null: false 
end 

Potem poszedł do przodu i kilka rekordów:

Dog.create(sits: false) 
Dog.create(sits: true) 
Dog.create(sits: true) #Dog record that will not be tied to a pet 
Cat.create(killed_mice: 2) 
Cat.create(killed_mice: 15) 
Cat.create(killed_mice: 15) #Cat record that will not be tied to a pet 
Bird.create 

A potem poszedł i wykonane niektórzy pet zapisy:

Pet.create(name: 'dog1', animal_id: 1, animal_type: "Dog") 
Pet.create(name: 'dog2', animal_id: 2, animal_type: "Dog") 
Pet.create(name: 'cat1', animal_id: 1, animal_type: "Cat") 
Pet.create(name: 'cat2', animal_id: 2, animal_type: "Cat") 
Pet.create(name: 'bird1', animal_id: 1, animal_type: "Bird") 

A to jest konfiguracja! Teraz trudna część: chcę stworzyć kilka zakresów w modelu Pet, które przekopują się w asocjacje polimorficzne.

Oto niektóre zakresy chciałbym napisać:

  • Daj mi wszystko Pets z animal_type == „Dog”, który może siedzieć
  • Daj mi wszystko Pets z animal_type == „Cat” które zabiły co najmniej 10 myszy
  • Daj mi wszystkie Pets, które NIE są oba typu animal_type "Pies" i nie mogą siedzieć. (Innymi słowy: Daj mi wszystkie zwierzęta: wszystkie z nich z wyjątkiem psów, które nie mogą siedzieć)

więc moim Pet modelu chciałbym umieścić moje zakresy tam:

class Pet < ActiveRecord::Base 
    belongs_to :animal, polymorphic: true 

    scope :sitting_dogs, -> {#query goes here} 
    scope :killer_cats, -> {#query goes here} 
    scope :remove_dogs_that_cannot_sit, -> {#query goes here} #only removes pet records of dogs that cannot sit. All other pet records are returned 
end 

Trudno jest napisać te zakresy.

Niektóre rzeczy, które znalazłem w Internecie, sprawiają wrażenie, że można pisać tylko te zakresy przy użyciu surowego kodu SQL. Zastanawiam się, czy możliwe jest użycie składni skrótu dla tych zakresów.

Wszelkie wskazówki/pomoc będą mile widziane!

Odpowiedz

1

Po przejrzeniu wcześniejszych odpowiedzi i bawi się z nim: oto co mam do pracy.

(Zauważ, że Pet.remove_dogs_that_cannot_sit zwraca tablicę. Metoda ta klasa jest czytelny, ale ma tę wadę, że są powolne z powodu N + 1. Wszelkie sugestie dotyczące ustalenia, że ​​byłoby bardzo mile widziane.)

class Dog < ActiveRecord::Base 
    has_many :pets, as: :animal 
    scope :sits, -> {where(sits: true)} 
end 

class Cat < ActiveRecord::Base 
    has_many :pets, as: :animal 
    scope :killer, ->{ where("killed_mice >= ?", 10) } 
end 

class Pet < ActiveRecord::Base 
    belongs_to :animal, polymorphic: true 

    scope :by_type, ->(type) {where(animal_type: type)} 
    scope :by_dogs, -> {by_type("Dog") } 
    scope :by_cats, -> {by_type("Cat") } 

    def self.sitting_dogs 
    all.by_dogs 
     .joins("INNER JOIN dogs on animal_type = 'Dog' and animal_id = dogs.id") 
     .merge(Dog.sits) 
    end 

    def self.killer_cats 
    all.by_cats 
     .joins("INNER JOIN cats on animal_type = 'Cat' and animal_id = cats.id") 
     .merge(Cat.killer) 
    end 

    # returns an Array not Pet::ActiveRecord_Relation 
    # slow due to N + 1 
    def self.remove_dogs_that_cannot_sit 
    all.reject{|pet| pet.animal_type == "Dog" && !pet.animal.sits} 
    end 
end 
+0

'self.remove_dogs_that_cannot_sit' może być wykonane szybciej za pomocą include na' animal'. Wypróbuj 'all.includes (: animal) .reject {| pet | pet.animal_type == "Dog" &&! pet.animal.sits} ' – chrismanderson

+0

zauważ, że jest subtelny, ale kiedy trafisz w część" .reject ", w rzeczywistości tracisz ruby ​​i pozostawiasz SQL za sobą, stąd powolne. Możesz łączyć zakresy AREL i pozostać w SQL, np.by_dogs.where (sits: false) powinno być równoważne, zakładając, że nie masz wartości NULL i domyślnie ustawiony jest false w kolumnie boolowskiej w db. IIRC, może się mylę, kiedy schodzę ze szczytu mojej głowy, ale ".all" jest dorozumiany i niepotrzebny, gdy używasz łańcuchów AREL, o ile masz zasięg. – engineerDave

0

bym dodać te zakresy do odpowiednich poszczególnych modeli np:

class Dog < ActiveRecord::Base 
    has_many :pets, as: :animal 
    scope :sits, ->{ where(sits: true) } 
end 
class Cat < ActiveRecord::Base 
    has_many :pets, as: :animal 
    scope :natural_born_killer, ->{ where("killed_mice >= ?", 10) } 
end 

jeśli potem trzeba je na głównym modelem dla zwierząt, można po prostu dodać je jako metody np:

class Pet < ActiveRecord::Base 
    belongs_to :animal, polymorphic: true 

    def sitting_dogs 
    where(:animal => Dog.sits.all) 
    end 
    def killer_cats 
    where(:animal => Cat.natural_born_killer.all) 
    end 
end 

itd.

Twój skomplikowany futerał to po prostu wszystkie zwierzęta domowe minus niektóre psy siedzące.

class Pet < ActiveRecord::Base 
    belongs_to :animal, polymorphic: true 
    scope :sits, ->{ where(sits: true) } 

    def sitting_dogs 
    where(:animal => Dog.sits.all) 
    end 

    # There's probably a nicer way than this - but it'll be functional 
    def remove_dogs_that_cannot_sit 
    where.not(:id => sitting_dogs.pluck(:id)).all 
    end 
end 
+0

Dziękuję za odpowiedź. Najtrudniejszą częścią jest to, co naprawdę mnie interesuje, to "Pet". Chcę wziąć kolekcję pet records i przefiltrować je w oparciu o zakresy. – Neil

+0

Tak - właśnie to robię teraz ... filtrujesz zwierzęta w oparciu o zakresy. –

+0

Uwaga: nie testowane na prawdziwych przykładach ... może zajść potrzeba manipulowania nimi, aby uzyskać efekt pracy. Uwaga: można również użyć scalania zakresu. –

1

Zgadzam się na indywidualne zakresy dla psów siedzących i kotów killer. Można wprowadzić zakres, w którym Pet będzie filtrował je według typu animal_type.

Oto moja wersja:

class Dog < ActiveRecord::Base 
    has_many :pets, as: :animal 
    scope :sits, ->{ where(sits: true) } 
end 

class Cat < ActiveRecord::Base 
    has_many :pets, as: :animal 
    scope :killer, ->{ where("killed_mice >= ?", 10) } 
end 

class Pet < ActiveRecord::Base 
    belongs_to :animal, polymorphic: true 
    scope :by_type, -> { |type| where(animal_type: type) } 
    scope :sitting_dogs, -> { by_type("Dog").sits } 
    scope :killer_cats, -> { by_type("Cat").killer } 
    scope :remove_dogs_that_cannot_sit, -> { reject{|pet| pet.animal_type == "Dog" && !pet.animal.sits} } 
end 
+0

dziękuję za odpowiedź! Wygląda świetnie, chociaż niestety to nie działa dla mnie. Występuje następujący błąd podczas korzystania z 'remove_dogs_that_cannot_sit' scope:' NameError: undefined lokalna zmienna lub metoda 'reject'' – Neil

+0

Wygląda na to, że trzeba określić w zakresie 'all.reject ...' aby to zrobić do pracy, choć nie jestem pewien dlaczego. – Neil

+1

Zauważyłem również, że zakres 'remove_dogs_that_cannot_sit' zwraca tablicę w przeciwieństwie do obiektu ActiveRecord :: Relation. Tak więc: prawdopodobnie nie powinien być zasięgiem, ponieważ nie można go przenosić. Być może powinna to być metoda klasowa. – Neil

1

Nie pełna odpowiedź, ale oto sposobem wykonywania kwerendy remove_dogs_that_cannot_sit zwracającą relacją AR i usuwa n + 1.

class Pet < ActiveRecord::Base 
    belongs_to :animal, polymorphic: true 
    belongs_to :dog, -> { where(pets: { animal_type: 'Dog' }) }, foreign_key: :animal_id 

    def self.remove_dogs_that_cannot_sit 
    includes(:dog).where.not("pets.animal_type = 'Dog' AND dogs.sits = false").references(:dogs) 
    end 

    def self.old_remove_dogs_that_cannot_sit 
    all.reject{|pet| pet.animal_type == "Dog" && !pet.animal.sits} 
    end 
end 

Korzystanie belongs_to na modelu polimorficznym to świetny sposób na przyspieszenie niektórych zapytań, zwłaszcza jeśli Twój model polimorficzny jest ograniczony do niewielkiej liczby opcji. Możesz również wyczyścić część swojej metody zasięgu na Pet.

def self.sitting_dogs 
    includes(:dog).merge(Dog.sits).references(:dogs) 
    end 

Szybciej.

irb(main):085:0> puts Benchmark.measure { 1000.times { Pet.remove_dogs_that_cannot_sit } } 
    0.040000 0.000000 0.040000 ( 0.032890) 
=> nil 

irb(main):087:0> puts Benchmark.measure { 1000.times { Pet.old_remove_dogs_that_cannot_sit } } 
    1.610000 0.090000 1.700000 ( 1.923665) 
=> nil 
0

Oto kolejny sposób usuwania N + 1 na remove_dogs_that_cannot_sit

scope :joins_all -> { 
    joins("left join cats on animal_type = 'Cat' and animal_id = cats.id") 
    .joins("left join dogs on animal_type = 'Dog' and animal_id = dogs.id") 
    .joins("left join birds on animal_type = 'Bird' and animal_id = birds.id") 
} 

Pet.join_all.where.not("animal_type = 'Dog' and sits = 'f'")