Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scopes and Queries with Polymorphic Associations

I have found very little about how one can write scopes for polymorphic associations in rails, let alone how to write queries on polymorphic associations.

In the Rails Documentation, I have looked at the Polymorphic Associations section, the Joining Tables section, and the Scopes section. I have also done my fair share of googling.

Take this setup for example:

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

So a Pet can be of animal_type "Dog", "Cat", or "Bird".

To show all the table structures: here is my 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

I then went ahead and made some records:

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

And then I went and made some pet records:

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

And that is the setup! Now the tough part: I want to create some scopes on the Pet model which dig into the polymorphic associations.

Here are some scopes I would like to write:

  • Give me all the Pets of animal_type == "Dog" that can sit
  • Give me all the Pets of animal_type == "Cat" that have killed at least 10 mice
  • Give me all the Pets that are NOT both animal_type "Dog" and cannot sit. (In other words: Give me all the pets: all of them: except for dogs that cannot sit)

So in my Pet model I would want to put my scopes in there:

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

I am finding it pretty tough to write these scopes.

Some stuff I found online makes it look like you can only write these scopes with raw SQL. I am wondering if it is possible to use the Hash syntax for these scopes instead.

Any tips/help would be greatly appreciated!

like image 524
Neil Avatar asked Dec 13 '16 02:12

Neil


People also ask

What is polymorphic association in Rails?

Polymorphic relationship in Rails refers to a type of Active Record association. This concept is used to attach a model to another model that can be of a different type by only having to define one association.

What is polymorphic association in Sequelize?

A polymorphic association consists on two (or more) associations happening with the same foreign key. For example, consider the models Image , Video and Comment . The first two represent something that a user might post. We want to allow comments to be placed in both of them.

How is polymorphic association set up in Rails?

The basic structure of a polymorphic association (PA)sets up 2 columns in the comment table. (This is different from a typical one-to-many association, where we'd only need one column that references the id's of the model it belongs to). For a PA, the first column we need to create is for the selected model.


2 Answers

I'd add these scopes to the relevant individual models eg:

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

if you then need them on the main Pet model, you can just add them as methods eg:

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

etc

Your complicated case is just all pets minus some that are also sitting dogs.

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
like image 70
Taryn East Avatar answered Oct 23 '22 01:10

Taryn East


I agree of having individual scopes for sitting dogs and killer cats. A scope could be introduced for Pet to filter them by animal_type.

Here's my version:

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
like image 43
nesiseka Avatar answered Oct 22 '22 23:10

nesiseka