Rails include a conditional polymorphic association - ruby-on-rails

I have a polymorphic association:
Following the rails association model picture from rails guide and Supposing employees is my Content and pictures is my translations i need perform an filtered association.
class Content < ApplicationRecord
has_many :translations,:as => :transl
end
class Translation < ApplicationRecord
enum idiom: [ :EN, :NO]
belongs_to :transl, :polymorphic => true
end
As a result I would like to have a json with my content but only with specific idiom for that i have something like this:
Content.all.to_json(:include => {:translations => {:only => [:text, :idiom],:where =>{idiom: "EN"}}})
I tried to create a method to be included on to_json, but methods inside it is meant to be only getters.
i tried also left outer join but it don't work as well:
SELECT "contents".* FROM "contents" LEFT OUTER JOIN "translations" ON "translations"."transl_id" = "contents"."id" WHERE (translations.idiom=0)
To work i must to perform a raw query enforcing to get some results from the left side i assume that could have some contents without translation such as:
SELECT "contents".* FROM "contents" LEFT OUTER JOIN "translations" ON "contents"."id" = "translations"."transl_id" **OR "contents"."id" >0** WHERE (translations.idiom="EN")
Is there a better way to do this?

SELECT "contents". FROM "contents" LEFT OUTER JOIN "translations" ON "contents"."id" = "translations"."transl_id" OR "contents"."id" >0 WHERE (translations.idiom="EN")*
can be ===>
#lang="EN"
Content.where(:id > 0).includes(:translations).where({:translations =>{:idiom=>#lang}})

Related

Joins Great Grandparent Model - Ruby On Rails

I'm sure this is very simple but I cannot get it to work. I have the following associations.
model Category
has_many :category_brands
end
model CategoryBrand
has_many :category_models
belongs_to :category
end
model CategoryModel
has_many :products
belongs_to :category_brand
end
model Product
belongs_to :category_model
end
In theory, I want to query all D records that have an A record with the name equal to "x". So like this:
#products = Product.joins(category_model: {category_brand: :category}).where("category.name like ?", "%Incline Motors%")
But I cannot get this to work. Any help would be appreciated.
Current Error:
G::UndefinedTable: ERROR: missing FROM-clause entry for table "category" LINE 1: ...es"."id" = "category_brands"."category_id" WHERE (category.n... ^ : SELECT COUNT(*) FROM "products" INNER JOIN "category_models" ON "category_models"."id" = "products"."category_model_id" INNER JOIN "category_brands" ON "category_brands"."id" = "category_models"."category_brand_id" INNER JOIN "categories" ON "categories"."id" = "category_brands"."category_id" WHERE (category.name like '%Incline Motors%')
The table name should be pluralised -- note the SQL statement text INNER JOIN "categories"
#products = Product.joins(category_model: {category_brand: :category}).where("categories.name like ?", "%Incline Motors%")

Rails ActiveRecord query using multiple joins involving polymorphic association

I'm trying to figure out how I can replicate the following SQL query using AR given the model definitions below. The cast is necessary to perform the average. The result set should group foo by bar (which comes from the polymorphic association). Any help is appreciated.
SQL:
SELECT AVG(CAST(r.foo AS decimal)) "Average", s.bar
FROM rotation r INNER JOIN cogs c ON r.cog_id = c.id
INNER JOIN sprockets s ON s.id = c.crankable_id
INNER JOIN machinists m ON r.machinist_id = m.id
WHERE c.crankable_type = 'Sprocket' AND
r.machine_id = 123 AND
m.shop_id = 1
GROUP BY s.bar
ActiveRecord Models:
class Rotation < ActiveRecord::Base
belongs_to :cog
belongs_to :machinist
belongs_to :machine
end
class Cog < ActiveRecord::Base
belongs_to :crankable, :polymorphic => true
has_many :rotation
end
class Sprocket < ActiveRecord::Base
has_many :cogs, :as => :crankable
end
class Machinist < ActiveRecord::Base
belongs_to :shop
end
UPDATE
I've figured out a way to make it work, but it feels like cheating. Is there are a better way than this?
Sprocket.joins('INNER JOIN cogs c ON c.crankable_id = sprockets.id',
'INNER JOIN rotations r ON r.cog_id = c.id',
'INNER JOIN machinists m ON r.machinist_id = m.id')
.select('sprockets.bar', 'r.foo')
.where(:r => {:machine_id => 123}, :m => {:shop_id => 1})
.group('sprockets.bar')
.average('CAST(r.foo AS decimal)')
SOLUTION
Albin's answer didn't work as-is, but did lead me to a working solution. First, I had a typo in Cog and had to change the relation from:
has_many :rotation
to the plural form:
has_many :rotations
With that in place, I am able to use the following query
Sprocket.joins(cogs: {rotations: :machinist})
.where({ machinists: { shop_id: 1 }, rotations: { machine_id: 123}})
.group(:bar)
.average('CAST(rotations.foo AS decimal)')
The only real difference is that I had to separate the where clause since a machine does not belong to a machinist. Thanks Albin!
I think this code is a little simpler and taking more help from AR
Sprocket
.joins(cogs: {rotations: :machinist})
.where({ machinists: { machine_id: 123, shop_id: 1 } } )
.group(:bar)
.average('CAST(rotations.foo AS decimal)')
The select clause was unnecessary, you don't have to select values since you only need them internally in the query, AR helps you decide what you need afterwards.
I tested this out using a similar structure in one of my own projects but it is not the exact same models so there might be a typo or something in there if it does not run straight up. I ran:
Activity
.joins(locations: {participants: :stuff})
.where({ stuffs: { my_field: 1 } })
.group(:title)
.average('CAST(participants.date_of_birth as decimal)')
producing this query
SELECT AVG(CAST(participants.date_of_birth as decimal)) AS average_cast_participants_date_of_birth_as_decimal, title AS title
FROM `activities`
INNER JOIN `locations` ON `locations`.`activity_id` = `activities`.`id`
INNER JOIN `participants` ON `participants`.`location_id` = `locations`.`id`
INNER JOIN `stuffs` ON `stuffs`.`id` = `participants`.`stuff_id`
WHERE `stuffs`.`my_field` = 1
GROUP BY title
which AR makes in to a hash looking like this:
{"dummy title"=>#<BigDecimal:7fe9fe44d3c0,'0.19652273E4',18(18)>, "stats test"=>nil}

Specifying conditions on eager loaded associations returns ActiveRecord::RecordNotFound

The problem is that when a Restaurant does not have any MenuItems that match the condition, ActiveRecord says it can't find the Restaurant. Here's the relevant code:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
def self.with_meals_of_the_week
includes({menu_items: :meal}).where(:'menu_items.date' => Time.now.beginning_of_week..Time.now.end_of_week)
end
end
And the sql code generated:
Restaurant Load (0.0ms)←[0m ←[1mSELECT DISTINCT "restaurants".id FROM "restaurants"
LEFT OUTER JOIN "menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN "meals" ON "meals"."id" = "menu_items"."meal_id" WHERE
"restaurants"."id" = ? AND ("menu_items"."date" BETWEEN '2012-10-14 23:00:00.000000'
AND '2012-10-21 22:59:59.999999') LIMIT 1←[0m [["id", "1"]]
However, according to this part of the Rails Guides, this shouldn't be happening:
Post.includes(:comments).where("comments.visible", true)
If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded.
The SQL generated is a correct translation of your query. But look at it,
just at the SQL level (i shortened it a bit):
SELECT *
FROM
"restaurants"
LEFT OUTER JOIN
"menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN
"meals" ON "meals"."id" = "menu_items"."meal_id"
WHERE
"restaurants"."id" = ?
AND
("menu_items"."date" BETWEEN '2012-10-14' AND '2012-10-21')
the left outer joins do the work you expect them to do: restaurants
are combined with menu_items and meals; if there is no menu_item to
go with a restaurant, the restaurant is still kept in the result, with
all the missing pieces (menu_items.id, menu_items.date, ...) filled in with NULL
now look aht the second part of the where: the BETWEEN operator demands,
that menu_items.date is not null! and this
is where you filter out all the restaurants without meals.
so we need to change the query in a way that makes having null-dates ok.
going back to ruby, you can write:
def self.with_meals_of_the_week
includes({menu_items: :meal})
.where('menu_items.date is NULL or menu_items.date between ? and ?',
Time.now.beginning_of_week,
Time.now.end_of_week
)
end
The resulting SQL is now
.... WHERE (menu_items.date is NULL or menu_items.date between '2012-10-21' and '2012-10-28')
and the restaurants without meals stay in.
As it is said in Rails Guide, all Posts in your query will be returned only if you will not use "where" clause with "includes", cause using "where" clause generates OUTER JOIN request to DB with WHERE by right outer table so DB will return nothing.
Such implementation is very helpful when you need some objects (all, or some of them - using where by base model) and if there are related models just get all of them, but if not - ok just get list of base models.
On other hand if you trying to use conditions on including tables then in most cases you want to select objects only with this conditions it means you want to select Restaurants only which has meals_items.
So in your case, if you still want to use only 2 queries (and not N+1) I would probably do something like this:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
cattr_accessor :meals_of_the_week
def self.with_meals_of_the_week
restaurants = Restaurant.all
meals_of_the_week = {}
MenuItems.includes(:meal).where(date: Time.now.beginning_of_week..Time.now.end_of_week, restaurant_id => restaurants).each do |menu_item|
meals_of_the_week[menu_item.restaurant_id] = menu_item
end
restaurants.each { |r| r.meals_of_the_week = meals_of_the_week[r.id] }
restaurants
end
end
Update: Rails 4 will raise Deprecation warning when you simply try to do conditions on models
Sorry for possible typo.
I think there is some misunderstanding of this
If there was no where condition, this would generate the normal set of two queries.
If, in the case of this includes query, there were no comments for any
posts, all the posts would still be loaded. By using joins (an INNER
JOIN), the join conditions must match, otherwise no records will be
returned.
[from guides]
I think this statements doesn't refer to the example Post.includes(:comments).where("comments.visible", true)
but refer to one without where statement Post.includes(:comments)
So all work right! This is the way LEFT OUTER JOIN work.
So... you wrote: "If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded." Ok! But this is true ONLY when there is NO where clause! You missed the context of the phrase.

named_scope + average is causing the table to be specified more then once in the sql query run on postgresql

I have a named scopes like so...
named_scope :gender, lambda { |gender| { :joins => {:survey_session => :profile }, :conditions => { :survey_sessions => { :profiles => { :gender => gender } } } } }
and when I call it everything works fine.
I also have this average method I call...
Answer.average(:rating, :include => {:survey_session => :profile}, :group => "profiles.career")
which also works fine if I call it like that.
However if I were to call it like so...
Answer.gender('m').average(:rating, :include => {:survey_session => :profile}, :group => "profiles.career")
I get...
ActiveRecord::StatementInvalid: PGError: ERROR: table name "profiles" specified more than once
: SELECT avg("answers".rating) AS avg_rating, profiles.career AS profiles_career FROM "answers" LEFT OUTER JOIN "survey_sessions" survey_sessions_answers ON "survey_sessions_answers".id = "answers".survey_session_id LEFT OUTER JOIN "profiles" ON "profiles".id = "survey_sessions_answers".profile_id INNER JOIN "survey_sessions" ON "survey_sessions".id = "answers".survey_session_id INNER JOIN "profiles" ON "profiles".id = "survey_sessions".profile_id WHERE ("profiles"."gender" = E'm') GROUP BY profiles.career
Which is a little hard to read but says I'm including the table profiles twice.
If I were to just remove the include from average it works but it isn't really practical because average is actually being called inside a method which gets passed the scoped. So there is some times gender or average might get called with out each other and if either was missing the profile include it wouldn't work.
So either I need to know how to fix this apparent bug in Rails or figure out a way to know what scopes were applied to a ActiveRecord::NamedScope::Scope object so that I could check to see if they have been applied and if not add the include for average.
Looks like ActiveRecord is generating some bad SQL:
SELECT avg("answers".rating) AS avg_rating,
profiles.career AS profiles_career
FROM "answers"
LEFT OUTER JOIN "survey_sessions" survey_sessions_answers
ON "survey_sessions_answers".id = "answers".survey_session_id
LEFT OUTER JOIN "profiles"
ON "profiles".id = "survey_sessions_answers".profile_id
INNER JOIN "survey_sessions"
ON "survey_sessions".id = "answers".survey_session_id
INNER JOIN "profiles"
ON "profiles".id = "survey_sessions".profile_id
WHERE ("profiles"."gender" = E'm')
GROUP BY profiles.career
Presumably it's generated the left joins as part of getting the projected property, and the inner joins as part of getting the criteria: this wouldn't be invalid (just inefficient) if it assigned aliases to those tables, but it doesn't. Is there a way to specify an alias name from your app?

How on earth is this rails query working?

I have just optimised some Ruby code that was in a controller method, replacing it with a direct database query. The replacement appears to work and is much faster. Thing is, I've no idea how Rails managed to figure out the correct query to use!
The purpose of the query is to work out tag counts for Place models within a certain distance of a given latitude and longitude. The distance part is handled by the GeoKit plugin (which basically adds convenience methods to add the appropriate trigonometry calculations to the select), and the tagging part is done by the acts_as_taggable_on_steroids plugin, which uses a polymorphic association.
Below is the original code:
places = Place.find(:all, :origin=>latlng, :order=>'distance asc', :within=>distance, :limit=>200)
tag_counts = MyTag.tagcounts(places)
deep_tag_counts=Array.new()
tag_counts.each do |tag|
count=Place.find_tagged_with(tag.name,:origin=>latlng, :order=>'distance asc', :within=>distance, :limit=>200).size
deep_tag_counts<<{:name=>tag.name,:count=>count}
end
where the MyTag class implements this:
def MyTag.tagcounts(places)
alltags = places.collect {|p| p.tags}.flatten.sort_by(&:name)
lasttag=nil;
tagcount=0;
result=Array.new
alltags.each do |tag|
unless (lasttag==nil || lasttag.name==tag.name)
result << MyTag.new(lasttag,tagcount)
tagcount=0
end
tagcount=tagcount+1
lasttag=tag
end
unless lasttag==nil then
result << MyTag.new(lasttag,tagcount)
end
result
end
This was my (very ugly) first attempt as I originally found it difficult to come up with the right rails incantations to get this done in SQL. The new replacement is this single line:
deep_tag_counts=Place.find(:all,:select=>'name,count(*) as count',:origin=>latlng,:within=>distance,:joins=>:tags, :group=>:tag_id)
Which results in an SQL query like this:
SELECT name,count(*) as count, (ACOS(least(1,COS(0.897378837271255)*COS(-0.0153398733287034)*COS(RADIANS(places.lat))*COS(RADIANS(places.lng))+
COS(0.897378837271255)*SIN(-0.0153398733287034)*COS(RADIANS(places.lat))*SIN(RADIANS(places.lng))+
SIN(0.897378837271255)*SIN(RADIANS(places.lat))))*3963.19)
AS distance FROM `places` INNER JOIN `taggings` ON (`places`.`id` = `taggings`.`taggable_id` AND `taggings`.`taggable_type` = 'Place') INNER JOIN `tags` ON (`tags`.`id` = `taggings`.`tag_id`) WHERE (places.lat>50.693170735732 AND places.lat<52.1388692642679 AND places.lng>-2.03785525810908 AND places.lng<0.280035258109084 AND (ACOS(least(1,COS(0.897378837271255)*COS(-0.0153398733287034)*COS(RADIANS(places.lat))*COS(RADIANS(places.lng))+
COS(0.897378837271255)*SIN(-0.0153398733287034)*COS(RADIANS(places.lat))*SIN(RADIANS(places.lng))+
SIN(0.897378837271255)*SIN(RADIANS(places.lat))))*3963.19)
<= 50) GROUP BY tag_id
Ignoring the trig (which is from GeoKit, and results from the :within and :origin parameters), what I can't figure out about this is how on earth Rails was able to figure out from the instruction to join 'tags', that it had to involve 'taggings' in the JOIN (which it does, as there is no direct way to join the places and tags tables), and also that it had to use the polymorphic stuff.
In other words, how the heck did it (correctly) come up with this bit:
INNER JOIN `taggings` ON (`places`.`id` = `taggings`.`taggable_id` AND `taggings`.`taggable_type` = 'Place') INNER JOIN `tags` ON (`tags`.`id` = `taggings`.`tag_id`)
...given that I never mentioned the taggings table in the code! Digging into the taggable plugin, the only clue that Rails has seems to be this:
class Tag < ActiveRecord::Base
has_many :taggings, :dependent=>:destroy
...
end
Anybody able to give some insight into the magic going on under the hood here?
The acts_as_taggable_on_steroids plugin tells your Place model that it has_many Tags through Taggings. With this association specified, ActiveRecord knows that it needs to join taggings in order to get to the tags table. The same thing holds true for HABTM relationships. For example:
class Person < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :people
end
>> Person.first(:joins => :tags)
This produces the following SQL:
SELECT "people".*
FROM "people"
INNER JOIN "people_tags" ON "people_tags".person_id = "people".id
INNER JOIN "tags" ON "tags".id = "people_tags".tag_id
LIMIT 1

Resources