Области и запросы с полиморфными ассоциациями

Я очень мало узнал о том, как можно писать области для полиморфных ассоциаций в рельсах, не говоря уже о том, как писать запросы на полиморфные ассоциации.

В документации Rails я просмотрел раздел Полиморфные ассоциации, раздел "Соединительные таблицы" и раздел "Области" . Я также сделал свою долю в googling.

Возьмите эту настройку, например:

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

Итак, Pet может быть animal_type "Собака", "Кошка" или "Птица".

Чтобы показать все структуры таблицы: вот мой 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

Затем я пошел вперед и сделал несколько записей:

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

И затем я пошел и сделал несколько записей Pet:

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")

И это настройка! Теперь сложная часть: я хочу создать некоторые области применения модели Pet, которые выкапывают в полиморфные ассоциации.

Вот несколько областей, которые я бы хотел написать:

  • Дайте мне все Pets animal_type == "Собака", которая может сидеть
  • Дайте мне все Pets animal_type == "Cat", которые убили не менее 10 мышей.
  • Дайте мне все Pets, которые не являются как animal_type "Dog", так и не могут сидеть. (Другими словами: Дайте мне всех домашних животных: все они: кроме собак, которые не могут сидеть).

Итак, в моей модели Pet я хотел бы разместить там свои области:

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

Мне сложно писать эти области.

Некоторые вещи, которые я нашел в Интернете, показывают, что вы можете только записывать эти области с помощью raw SQL. Мне интересно, можно ли вместо этого использовать синтаксис Hash для этих областей.

Любые советы/помощь будут очень благодарны!

Ответ 1

После просмотра предыдущих ответов и игры с ним: вот что я должен работать.

(Обратите внимание, что Pet.remove_dogs_that_cannot_sit возвращает массив.Этот метод класса читабельен, но имеет недостаток в том, что он медленный из-за N + 1. Любые предложения по его исправлению будут очень благодарны.)

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

Ответ 2

Я согласен с тем, что у меня есть отдельные области для сидения собак и убийц. Сфера может быть введена для Pet для фильтрации их по типу animal_type.

Здесь моя версия:

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

Ответ 3

Не полный ответ, но вот способ выполнения запроса remove_dogs_that_cannot_sit, который возвращает отношение AR и удаляет 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

Использование belongs_to в полиморфной модели - отличный способ ускорить некоторые запросы, особенно если ваша полиморфная модель ограничена небольшим количеством опций. Вы можете очистить некоторые из ваших методов на Pet.

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

Быстрее тоже.

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

Ответ 4

Я бы добавил эти области к соответствующим отдельным моделям, например:

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

если они вам понадобятся в основной модели Pet, вы можете просто добавить их как методы, например:

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

и т.д.

Ваш сложный случай - это все домашние животные минус некоторые, которые также являются собаками.

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

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

  # There 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

Ответ 5

Вот еще один способ удаления N + 1 на 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'")

Ответ 6

То, что я сделал, это как ниже:

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

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

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

  scope :sitting_dogs, -> do
    joins("INNER JOIN dogs on \
    pets.animal_id = dogs.id and pets.animal_type = \
    'Dog'").merge(Dog.sittable)
  end

  scope :amok_cats, -> do
    joins("INNER JOIN cats on \
    pets.animal_id = cats.id and pets.animal_type = \
    'Cat'").merge(Cat.amok)
  end

   scope :can_sit_dogs, -> do
    joins("INNER JOIN dogs on \
    pets.animal_id = dogs.id and pets.animal_type = \
    'Dog'").merge(Dog.dissittable)
  end
end

Кроме того, имя scope более склонно к adjective, чем к noun. Поэтому я использую sittable dissitable amok вместо sits killer.

Если вы знакомы с вымогателем, вы также можете использовать его для поиска по проблеме

Желание помогло тебе.