Rails: Postgresql where with multiple conditions with join (polymorphic) - ruby-on-rails

Hi guys here is my code:
class Tailor < ActiveRecord::Base
has_many :tailor_items
has_many :order_items
[:collars, :sexes, :sleeves].each do |attribute|
has_many attribute, through: :tailor_items, source: :item, source_type: attribute.to_s.classify
end
end
class TailorItem < ActiveRecord::Base
belongs_to :tailor
belongs_to :item, polymorphic: true
end
class Collar < ActiveRecord::Base
end
What I need to do is this:
For a given shirt I need to select a tailor. A shirt can have a collar, male/female or a certain type of sleeve. Some tailors can make all collars but only a few sleeves, others can make only male stuff, etc.
The priority doesnt matter for this example. The idea is that I end up with 1 tailor.
I tried this:
tailors = Tailor.joins(:tailor_items).where("(item_id = ? and item_type = ?)",1,"Collar")
if tailors.count > 1
tailors.where("(item_id = ? and item_type = ?)",2,"Sleeve")
if tailors.count > 1
# and so forth.
end
end
But I never get a row back.
If I say:
Tailor.find(1).tailor_items
I get two results (sudo code for simplicity)
<id: 1, item_type: "Collar"><id:2, item_type:"Sleeve">
and for second tailor:
Tailor.find(2).tailor_items
I get two results (sudo code for simplicity)
<id: 1, item_type: "Collar"><id:3, item_type:"Sleeve">
but when I try to chain them in the query its no worky...
Not even if I put it all in one where:
Tailor.where("(item_id = 1 and item_type = 'Collar') and (item_id = 2 and item_type = 'Sleeve')")
I still get 0 results.
Tailor.where("item_id = 1 and item_type = 'Collar'") returns: Tailor #1
Tailor.where("item_id = 2 and item_type = 'Sleeve'") returns: Tailor #1
but together they return nothing.
Tailor Load (0.0ms) SELECT "tailors".* FROM "tailors" INNER
JOIN "tailor_items" ON "tailor_items"."tailor_id" = "tailors"."id" WHERE ((tailo
r_items.item_id = 1 and tailor_items.item_type = 'Collar') and (tailor_items.ite
m_id = 2 and tailor_items.item_type = 'Sleeve'))
I am confused..
Thanks for your help.
I run:
Win XP
Postgresql
Rails 3.2.2
PS: The only thing missing to make this complete after a polymorphic join is a bit of XML. :P Otherwise its just not enterprise-y enough..
EDIT:
Implementing Rob di Marcos scope, I get this SQL:
SELECT "tailors".* FROM "tailors" WHERE
(EXISTS(SELECT * FROM tailor_items WHERE tailor_items.item_id = 1 and tailor_items.item_type = 'Collar'))
AND (exists(select * from tailor_items where tailor_items.item_id = 2 and tailor_items.item_type = 'Sleeve'))
This returns
2 tailors instead of only tailor 1 who can do both (while tailor 2 cant do sleeve #2)

The problem is that the where needs to match on two rows. I generally will use sub-queries to test for this. So something like
Tailor.where("exists (select 'x' from tailor_items where
tailor_id = tailors.id and tailor_items.item_id = ? and
tailor_items.item_type=?)", 1, 'Collar').
where("exists (select 'x' from tailor_items where
tailor_id = tailors.id and tailor_items.item_id = ? and
tailor_items.item_type=?)", 2, 'Sleeve')
In this example, I have one sub-query for each tailor item I am looking for. I could easily make this a scope on Tailor like:
class Tailor
# ....
scope :with_item, lambda{ |item_id, item_type |
where("exists (select 'x' from tailor_items where
tailor_id = tailors.id and tailor_items.item_id = ? and
tailor_items.item_type=?)", item_id, item_type)
}
and then be able to chain my Tailor request
Tailor.with_item(1, 'Collar').with_item(2, 'Sleeve')

Related

ActiveRecord::AssociationRelation loses foreign key value when combined with a UNION

I'm trying to write a named scope that uses a union:
# Simplified example
class MyModel < ActiveRecord::Base
belongs_to :owner
scope :my_named_scope, -> {
scope_a = where("#{arel_table.name}.a = 'foo'")
scope_b = where("#{arel_table.name}.b = 'bar'")
union = scope_a.union(scope_b)
from(union)
}
end
MyModel.my_named_scope works fine
owner.my_models.my_named_scope does not
In the failing case, I can call .to_sql on the ActiveRecord::AssociationRelation which my_named_scope returns, and that SQL has a syntax error:
SELECT "my_models".* FROM ( SELECT "my_models".* FROM "my_models" WHERE "my_models"."owner_id" = 1001 AND (my_models.a = 'foo') UNION SELECT "my_models".* FROM "my_models" WHERE "my_models"."owner_id" = AND (my_models.b = 'bar') ) WHERE "my_models"."owner_id" =
...The sql just cuts off after "owner_id" = as if it can't find the owner_id, even though the owner_id (1001) appears earlier in the query. How do I correct this?
TMI: Rails 4.2 (I know it's old. I'm at a big company with big tech debt.)

How can I make a outer join in a complex Rails has_many scope?

My goal is to make a scope where I can see all the completed courses of a Member object.
A Course is composed of many Sections. There is one Quiz per Section. And every single Quiz must be set to complete in order for the Course to be complete.
For example :
Course
Section A -> Quiz A ( Complete )
Section B -> Quiz B ( Complete )
Section C -> Quiz C ( Complete )
This is my best attempt at writing this kind of scope :
# Member.rb
has_many :completed_courses, -> {
joins(:quizzes, :sections)
.where(quizzes: {completed: true})
.group('members.id HAVING count(sections.quizzes.id) = count(sections.id)')
}, through: :course_members, source: :course
The part that I'm missing is this part count(sections.quizzes.id) which isn't actually SQL. I'm not entirely sure what kind of JOIN this would be called, but I need some way to count the completed quizzes that belong to the course and compare that number to how many sections. If they are equal, that means all the quizzes have been completed.
To be fair, just knowing the name of this kind of JOIN would probably set me in the right direction.
Update
I tried using #jamesdevar 's response :
has_many :completed_courses, -> {
joins(:sections)
.joins('LEFT JOIN quizzes ON quizzes.section_id = sections.id')
.having('COUNT(sections.id) = SUM(CASE WHEN quizzes.completed = true THEN 1 ELSE 0 END)')
.group('courses.id')
}, through: :course_members, source: :course
But it returns a [ ] value when it shouldn't. For example I have this data :
-> Course{id: 1}
-> Section{id: 1, course_id: 1}
-> Quiz{section_id: 1, member_id: 1, completed: true}
The Course has one Section in total. The Section may have hundreds of Quizzes associated with it from other Members, but for this specific Member, his Quiz is completed.
I think it has something to do with this SQL comparison not being unique to the individual member.
.having('COUNT(sections.id) = SUM(CASE WHEN quizzes.completed = true THEN 1 ELSE 0 END)')
The actual SQL produced is this :
SELECT "courses".* FROM "courses"
INNER JOIN "sections" ON "sections"."course_id" = "courses"."id"
INNER JOIN "course_members" ON "courses"."id" = "course_members"."course_id"
LEFT JOIN quizzes ON quizzes.section_id = sections.id
WHERE "course_members"."member_id" = $1
GROUP BY courses.id
HAVING COUNT(sections.id) = SUM(CASE WHEN quizzes.completed = true THEN 1 ELSE 0 END)
ORDER BY "courses"."title"
ASC [["member_id", 1121230]]
has_many :completed_courses, -> {
joins(:sections)
.joins('LEFT JOIN quizzes ON quizzes.section_id = sections.id')
.having('COUNT(sections.id) = SUM(CASE WHEN quizzes.completed = true THEN 1 ELSE 0 END)')
.group('courses.id')
}, through: :course_members, source: :course
In this case having will filter each cources.id group by condition when each section has completed quizz.

How to stop ActiveRecord from combining queries?

Stripping out the irrelevant parts, I have the following models:
class Member < ActiveRecord::Base
attr_accessible :custom_fields_attributes
has_many :custom_fields, :as => :custom_fieldable
end
class CustomField < ActiveRecord::Base
attr_accessible :key_id, :value
belongs_to :key
belongs_to :custom_fieldable, :polymorphic => true
end
class Key < ActiveRecord::Base
attr_accessible :key
end
Suppose I have two entries in my keys table:
---------------
id | key
---+-----------
1 | Hair color
2 | Height
how do I run a query that would let me retrieve all members with "Brown" hair color and "5ft" in height?
I tried doing it as two successive commands:
ms = Member.joins(:custom_fields).where("custom_fields.key_id = 1 AND custom_fields.value = 'Brown'")
ms.joins(:custom_fields).where("custom_fields.key_id = 2 AND custom_fields.value = '5ft'")
However, this doesn't work because ActiveRecord runs the second query like this:
SELECT `members`.* FROM `members` INNER JOIN `custom_fields` ON
`custom_fields`.`custom_fieldable_id` = `members`.`id` AND
`custom_fields`.`custom_fieldable_type` = 'Member' WHERE
(custom_fields.key_id = 1 AND custom_fields.value = 'Brown') AND
(custom_fields.key_id = 2 AND custom_fields.value = '5ft')
The query above returns an empty record because no custom field can be simultaneously two things at once.
What I would like to do is to have Rails evaluate the first result, and then run the second query on it. How would I do that?
The version I'm using is Rails 3.2.14.
I suspect you will have to alias one (or both) of the times you're joining the custom_fields table if you want to do it in one query.
Something like:
Member
.joins('custom_fields as custom_fields_1').where("custom_fields_1.key_id = 1 AND custom_fields_1.value = 'Brown'")
.joins('custom_fields as custom_fields_2').where("custom_fields_2.key_id = 2 AND custom_fields_2.value = '5ft'")
Alternatively, run the two queries separately, and merge the results. eg.
brown_members = Member.joins(:custom_fields).where("custom_fields.key_id = 1 AND custom_fields.value = 'Brown'")
height_members = Member.joins(:custom_fields).where("custom_fields.key_id = 2 AND custom_fields.value = '5ft'")
ms = brown_members.merge(height_members)

ActiveRecord select add count

In my ActiveRecord query, I need to provide this info in the select method:
(SELECT count(*) from likes where likes.spentit_id = spentits.id) as like_count,
(SELECT count(*) from comments where comments.spentit_id = spentits.id) as comment_count
Of course, I pass pass these two as string to the .select() part, but I am wondering what's the proper/alternative way of doing this?
Here's the complete query I am trying to call:
SELECT DISTINCT
spentits.*,
username,
(SELECT count(*) from likes where likes.spentit_id = spentits.id) as like_count,
(SELECT count(*) from comments where comments.spentit_id = spentits.id) as comment_count,
(SELECT count(*) from wishlist_items where wishlist_items.spentit_id = spentits.id) as wishlist_count,
(case when likes.id is null then 0 else 1 end) as is_liked_by_me,
(case when wishlist_items.id is null then 0 else 1 end) as is_wishlisted_by_me,
(case when comments.id is null then 0 else 1 end) as is_commented_by_me
FROM spentits
LEFT JOIN users ON users.id = spentits.user_id
LEFT JOIN likes ON likes.user_id = 9 AND likes.spentit_id = spentits.id
LEFT JOIN wishlist_items ON wishlist_items.user_id = 9 AND wishlist_items.spentit_id = spentits.id
LEFT JOIN comments ON comments.user_id = 9 AND comments.spentit_id = spentits.id
WHERE spentits.user_id IN
(SELECT follows.following_id
FROM follows
WHERE follows.follower_id = 9 AND follows.accepted = 1)
ORDER BY id DESC LIMIT 15 OFFSET 0;
All the tables here have their respective ActiveRecord object. Just really confused how to convert this query into 'activerecord'/rails way with writing least amount of SQL. The '9' user_id is suppose to be a parameter.
Update:
Ok so here's what I did inmean time, it's much better than raw SQL statement, but it still looks ugly to me:
class Spentit < ActiveRecord::Base
belongs_to :user
has_many :likes
has_many :wishlist_items
has_many :comments
scope :include_author_info, lambda {
joins([:user]).
select("username").
select("users.photo_uri as user_photo_uri").
select("spentits.*")
}
scope :include_counts, lambda {
select("(SELECT count(*) from likes where likes.spentit_id = spentits.id) as like_count").
select("(SELECT count(*) from comments where comments.spentit_id = spentits.id) as comment_count").
select("(SELECT count(*) from wishlist_items where wishlist_items.spentit_id = spentits.id) as wishlist_items_count").
select("spentits.*")
}
end
Using these scope methods, I can do:
Spentit.where(:id => 7520).include_counts.include_author_info.customize_for_user(45)
A bit about the classes. A User has many Spentits. A Spentit has many comments, likes and comments.
Ok, you're "doing it wrong", a little bit. Rather than
scope :include_counts, lambda {
select("(SELECT count(*) from likes where likes.spentit_id = spentits.id) as like_count").
select("(SELECT count(*) from comments where comments.spentit_id = spentits.id) as comment_count").
select("(SELECT count(*) from wishlist_items where wishlist_items.spentit_id = spentits.id) as wishlist_items_count").
select("spentits.*")
}
do
Spentit.find(7520).likes.count
Spentit.find(7520).wishlist_items.count
Spentit.find(7520).comments.count
Instead of
scope :include_author_info, lambda {
joins([:user]).
select("username").
select("users.photo_uri as user_photo_uri").
select("spentits.*")
}
do
Spentit.find(7520).user.username
Spentit.find(7520).user.photo_uri
Also, you can define scopes within the referenced models, and use those:
class Follow < ActiveRecord::Base
belongs_to :follower, :class_name => "User"
belongs_to :following, :class_name => "User"
scope :accepted, lambda{ where(:accepted => 1) }
end
Spentits.where(:user => Follow.where(:follower => User.find(9)).accepted)
Now, maybe you also do:
class Spentit
def to_hash
hash = self.attributes
hash[:like_count] = self.like.count
# ...
end
end
but you don't need to do anything fancy to get those counts "under normal circumstances", you already have them.
Note, however, you'll probably also want to do eager loading, which you can apparently make as part of the default scope, or you'll do a lot more queries than you need.

How can I write the most efficient scope for asking only those parent objects which have some certain child objects

I have:
class Evaluation < ActiveRecord::Base
has_many :scores
end
class Score < ActiveRecord::Base
scope :for_disability_and_score,
lambda { |disability, score|
where('total_score >= ? AND name = ?', score, disability)
}
end
The scores table has a total_score field and a name field.
How can i write a scope to ask for only those evaluations which have a Score with name 'vision' and total_score of 2 and they have another Score with name 'hearing' and total_score of 3.
And how can all this be generalized to ask for those evaluations which have n scores with my parameters?
In raw sql it would be like:
I managed to do it in raw sql:
sql = %q-SELECT "evaluations".*
FROM "evaluations"
INNER JOIN "scores" AS s1 ON "s1"."evaluation_id" = "evaluations"."id"
INNER JOIN "target_disabilities" AS t1 ON "t1"."id" = "s1"."target_disability_id"
INNER JOIN "scores" AS s2 ON "s2"."evaluation_id" = "evaluations"."id"
INNER JOIN "target_disabilities" AS t2 ON "t2"."id" = "s2"."target_disability_id"
WHERE "t1"."name" = 'vision' AND (s1.total_score >= 1)
AND "t2"."name" = 'hearing' AND (s2.total_score >= 2)-
Here the point is to repeat this:
INNER JOIN "scores" AS s1 ON "s1"."evaluation_id" = "evaluations"."id"
and this (by replacing s1 to s2 and s3 and so forth):
WHERE (s1.total_score >= 1)
But it should be a rails way of doing this... :) Hopefully
Try this:
scope :by_scores, lambda do |params|
if params.is_a? Hash
query = joins(:scores)
params.each_pair do |name, score|
query = query.where( 'scores.total_score >= ? AND scores.name = ?', score, name)
end
query
end
end
Then call like this:
Evaluation.by_scores 'vision' => 2, 'hearing' => 3

Resources