Scopes and Queries with Polymorphic Associations - ruby-on-rails

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!

After reviewing previous answers and playing around with it: here is what I got to work.
(Note that Pet.remove_dogs_that_cannot_sit returns an array. This class method is readable but has the drawback of being slow due to N + 1. Any suggestions for fixing that would be greatly appreciated.)
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

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

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

Not the complete answer, but here's a way of executing the remove_dogs_that_cannot_sit query that returns an AR relation and removes the 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
Using a belongs_to on a polymorphic model is a great way of speeding up certain queries, especially if your polymorphic model is limited to a small number of options. You can clean up some of your scoped method on Pet as well.
def self.sitting_dogs
includes(:dog).merge(Dog.sits).references(:dogs)
end
Faster too.
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

Here is another way of removing N+1 on 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'")

What I did is like bellow:
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
Besides, scope name is more inclined to adjective rather than a noun. So, I use sittable dissitable amok instead of sits killer.
If you are familiar with ransack, you can also it for search based on the issue
Wish helped you.

Related

Get id, name from master table - Ruby

The association is has follows
Company has_many company_commodities
CompanyCommodity belongs to Company
CompanyCommodity belongs to Commodity
Consider that company1 has an entry in the company_commodities table.
Now in the decorator file, i need to get the commodity name and id of that record.
I have implemented as follows.
company1 = Company.find(1)
arr = co.company_commodities.map(&:commodity).pluck(:name, :id)
arr.map { |a| { name: a[0], id: a[1] } }
This produces the output as
[{:name=>"Pharmaceuticals", :id=>25},
{:name=>"Medical Devices", :id=>26}]
Is there a cleaner way to do this? 
class Company < ApplicationRecord
has_many :company_commodities
has_many :commodities, through: :company_commodities
end
class CompanyCommodity < ApplicationRecord
belongs_to :commodity
belongs_to :company
end
company = Company.find(1)
# this returns the object you can access by arr.first.id, arr.first.name
arr = company.commodities.select(:name, :id)
# if you want to access as hash, usage: arr.first['id'], arr.first['name']
arr = company.commodities.select(:name, :id).as_json
You can get the commodities association by using through, then you can use select to filter the attributes.
Change the associations as below
class Company
has_many :company_commodities
has_many :commodities, through: :company_commodities
end
class CompanyCommodity
belongs_to :company
belongs_to :commodity
end
And query the records as
company1 = Company.find(1)
arr = co.commodities.pluck(:name, :id)
arr.reduce([]) { |array, el| array << [[:name, :id], el].to_h}

rails Calculation (price * quantity)

Hi Im creating an ec site in my rails.
My migration:
(Item) has :name and :price.
(Basket_Item) has :item_id(fk), :basket_id(fk) and :quantity.
The system User will add some items to their basket.
So Basket_items is JOIN Table between (Item) and (Basket)
see like below.
What I want to do:
Get a price of Item and get a quantity from Basket_Items which is selected by user. Then I want to create #total_price = item_price * item_quantity.
Can anyone help me to create the #total_price please.
This is my a try code but it doesn't work on rails console.
Basket_items
class CreateBasketItems < ActiveRecord::Migration[5.2]
def change
create_table :basket_items do |t|
t.references :basket, index: true, null: false, foreign_key: true
t.references :item, index: true, null: false, foreign_key: true
t.integer :quantity, null: false, default: 1
t.timestamps
end
end
end
///
Items
class CreateItems < ActiveRecord::Migration[5.2]
def change
create_table :items do |t|
t.references :admin, index: true, null: false, foreign_key: true
t.string :name, null: false, index: true
t.integer :price, null: false
t.text :message
t.string :category, index: true
t.string :img
t.string :Video_url
t.text :discription
t.timestamps
end
end
end
///
This is my try a code but it doesn't work on rails console.
basket = current_user.prepare_basket
item_ids = basket.basket_items.select(:item_id)
items = basket.items.where(id: item_ids)
items_price = items.select(:price)
items_quantity = basket.basket_items.where(item_id: item_ids).pluck(:quantity)
def self.total(items_price, items_quantity)
sum(items_price * items_quantity)
end
#total_price = basket.total(items_price, item_quantity)
There are a few issues with your code:
You are trying to call a class method on an instance of the class. That's not gonna work, second you are passing in arrays into the calculation.
basket = current_user.prepare_basket
item_ids = basket.basket_items.select(:item_id)
items = basket.items.where(id: item_ids)
items_price = items.select(:price) # => Array of prices from the items in the basket
items_quantity = basket.basket_items.where(item_id: item_ids).pluck(:quantity) # => Array of quantities from the items in the basket
def self.total(items_price, items_quantity)
sum(items_price * items_quantity) # => So this line will do: sum(['12,95', '9.99'] * [1, 3])
end
#total_price = basket.total(items_price, item_quantity)
As you can see, that ain't gonna work. First you need to change the method and remove the self.
def total(items_price, items_quantity)
# ...
end
Now you can call the total method on a basket object: basket.total(items_price, items_quantity)
And inside the total method you need to loop through each items to do the calculation and add all the results.
def total(items_price, items_quantity)
total_price = 0
items_price.each_with_index do |price, index|
total_price += price * items_quantity[index]
end
total_price
end
But this solution could also fail, because you don't know sure that the order in the items_price is matching with the order of items_quantity. So a better approach would be to do the calculation for each basket_item seperate.
# Basket model
def total
total_price = 0
basket_items.each do |basket_item|
total_price += basket_item.total_price
end
total_price
end
# BasketItem model
def total_price
quantity * item.price
end
Now you can call it like this: basket.total

Rails 4 + Postgresql - Joins table with 2 conditions

I need to list Inputs that have Translations with language_id = 1 and dont have Translations with language_id = 2
My current scope is:
scope :language, -> (id){joins(:translations).where('translations.language_id = 1)}
Models:
class Input < ActiveRecord::Base
has_many :translations
has_many :languages, through: :translations
scope :language, -> (id) {joins(:translations).where('translations.language_id = 1')}
def translation_from_language(language)
self.translations.where(language: language).take
end
end
class Translation < ActiveRecord::Base
belongs_to :input
belongs_to :language
before_save :default_values
#Apenas para testar o scope de search
scope :search, -> (part) { where("LOWER(value) LIKE ?", "%#{part.downcase}%") }
# Se nao setar um input manualmente, irá criar um novo
def default_values
self.input ||= Input.create
end
end
class Language < ActiveRecord::Base
has_many :translations
has_many :inputs, through: :translations
end
So my solution is just create a query like
Input.where("id IN ( SELECT input_id
FROM translations
WHERE language_id = 1
)
AND id NOT IN (
SELECT input_id
FROM translations
WHERE language_id = 2
)")

Rails order by a field in parent belongs_to association

I have three models in my Rails app, User, Number, and Message:
class User < ActiveRecord::Base
has_many :numbers
has_many :messages, through: :numbers
end
class Number < ActiveRecord::Base
belongs_to :user
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :number
end
Number migration file has:
t.string :digits, index: true # Example: '14051234567' (no + sign)
In my controller:
sort_mode = # asc or desc
#messages = current_user.messages.order(???)
The thing is that I want to sort those messages by their numbers' digits.
How to do that dynamically (depending on sort_mode)?
EDIT:
sort_mode = 'asc'
#messages = current_user.messages.includes(:number)
order = { number: { digits: sort_mode } }
#messages = #messages.order(order)
^ Doesn't work. Second argument must be a direction.
Also, order('number.digits': sort_mode) throws:
SQLite3::SQLException: no such column: messages.number.digits: SELECT "messages".* FROM "messages" INNER JOIN "numbers" ON "messages"."number_id" = "numbers"."id" WHERE "numbers"."user_id" = ? ORDER BY "messages"."number.digits" ASC LIMIT 10 OFFSET 0
You'll need to use includes. Try:
#messages = current_user.messages.includes(:number).order('numbers.digits ASC')

ActiveRecord scope using class_name

I have a model to keep 2 users "user" and "worker".
create_table "action_logs", :force => true do |t|
t.integer "user_id"
t.integer "worker_id"
t.string "text_log"
end
class ActionLog < ActiveRecord::Base
belongs_to :user
belongs_to :worker, :class_name => 'User'
end
Now, I want to write a scope using "user" and "worker" on this model.
scope :not_inhouse, -> {
includes(:user).where( "users.inhouse = ?", false).
includes(:worker).where( "workers.inhouse = ?", false)
}
But it doesn't work.
ActiveRecord::StatementInvalid:
SQLite3::SQLException: no such column: workers.inhouse:
SELECT COUNT(DISTINCT "action_logs"."id") FROM "action_logs" LEFT OUTER JOIN "users" ON "users"."id" = "action_logs"."user_id" LEFT OUTER JOIN "users" "workers_action_logs" ON "workers_action_logs"."id" = "action_logs"."worker_id" WHERE (users.inhouse = 'f') AND (workers.inhouse = 'f')
It seems ActiveRecord doesn't handle class_name as I expected.
Is there any way to write a scope using class_name?
Thanks for comments. Now I understood 2 points.
Diffrence between includes and joins.
ActiveRecords name joined table name, independently of class_name.
So I can write the scope like this:
scope :not_inhouse, -> {
joins(:user).where( "users.inhouse = ?", false).
joins(:worker).where( "workers_action_logs.inhouse = ?", false)
}
or
scope :not_inhouse, -> {
joins(:user).where( "users.inhouse = ?", false).
joins('INNER JOIN "users" "workers" ON workers.id = action_logs.worker_id').
where( "workers.inhouse = ?", false)
}

Resources