【问题标题】:Scopes and Queries with Polymorphic Associations具有多态关联的范围和查询
【发布时间】:2019-09-30 16:09:55
【问题描述】:

关于如何在 Rails 中为多态关联编写作用域,我发现的很少,更不用说如何编写对多态关联的查询了。

在 Rails 文档中,我查看了 Polymorphic Associations sectionJoining Tables sectionScopes section。我也做了相当多的谷歌搜索。

以此设置为例:

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 模型上创建一些作用域,以挖掘多态关联。

这里有一些我想写的范围:

  • 把所有能坐的animal_type == "Dog"的Pets都给我
  • 给我所有的 Pets 的 animal_type == "Cat" 至少杀死了 10 只老鼠
  • 给我所有的Pets 既不是动物类型“狗”又不能坐的。 (换句话说:给我所有的宠物:所有的宠物:除了不能坐的狗)

所以在我的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

我发现编写这些范围非常困难。

我在网上找到的一些东西使您看起来只能使用原始 SQL 编写这些范围。我想知道是否可以对这些范围使用 Hash 语法。

任何提示/帮助将不胜感激!

【问题讨论】:

    标签: ruby-on-rails ruby


    【解决方案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 :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'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
    

    【讨论】:

    • 感谢您的回复。最困难的部分是我真正关心的是Pet 记录。我想收集一些宠物记录并根据范围过滤它们。
    • 是的 - 这就是我现在正在做的事情......您正在根据范围过滤宠物。
    • 注意:未在实际示例中进行测试...您可能需要摆弄它们才能使下摆正常工作。注意:你也可以使用范围合并。
    • 我的主要观点是,您不一定需要 Pet 模型上的范围来满足您的要求。
    • 您的sitting_dogs 方法会是类方法而不是实例方法吗?另外:我不知道您可以像使用sits 范围那样编写范围。这很有趣,因为您不能在 Pet 类上调用 sits 范围。相反,它看起来像是一个方便的范围,您实际上需要在 sitting_dogs 方法中使用它。
    【解决方案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
    

    【讨论】:

    • 感谢您的回答!它看起来很棒,但不幸的是它对我不起作用。使用remove_dogs_that_cannot_sit范围时出现以下错误:NameError: undefined local variable or method 'reject'
    • 看起来需要在all.reject... 范围内指定才能使其工作,但我不知道为什么。
    • 我还注意到您的 remove_dogs_that_cannot_sit 范围返回一个数组,而不是一个 ActiveRecord::Relation 对象。因此:它可能不应该是一个范围,因为它不可链接。它也许应该是一个类方法。
    【解决方案3】:

    在查看了以前的答案并进行了尝试之后:这就是我要工作的内容。

    (请注意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
    

    【讨论】:

    • self.remove_dogs_that_cannot_sit 可以通过在 animal 上使用包含来更快。试试all.includes(:animal).reject{|pet| pet.animal_type == "Dog" &amp;&amp; !pet.animal.sits}
    • 请注意它的微妙之处,但是当您点击“.reject”部分时,您实际上是在使用 ruby​​ 并将 SQL 抛在后面,因此它的速度很慢。您可以链接 AREL 范围并留在 SQL 中,例如by_dogs.where(sits: false) 应该是等效的,假设您在 db 的布尔列上设置了 NOT NULL 和默认 false 。 IIRC,我可能是错的,因为我想不通,但是只要你有一个范围,当你链接 AREL 范围时,“.all”是隐含的并且是不必要的。
    【解决方案4】:

    不是完整的答案,但这是一种执行 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
    

    【讨论】:

      【解决方案5】:

      这是在remove_dogs_that_cannot_sit上删除N+1的另一种方法

      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

        如果您熟悉ransack,也可以根据issue进行搜索

        希望对你有所帮助。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-07-22
          • 1970-01-01
          • 2018-04-24
          • 2013-07-22
          • 1970-01-01
          • 1970-01-01
          • 2017-10-22
          • 1970-01-01
          相关资源
          最近更新 更多