JOIN statement in Rails with OR condition across 2 tables - ruby-on-rails

UPDATE 2:
This looks much better:
Comp.includes(:members).where('members.member_email = ? OR comps.user_id = ?', current_user.email,current_user.id)
UPDATE:
This seems to work but is there a more elegant way to do this in Rails? I feel like there must be.
#my_comps = Comp.joins('LEFT OUTER JOIN teams ON teams.comp_id = comps.id LEFT OUTER JOIN members ON members.team_id = teams.id').where('members.member_email = ? OR comps.user_id = ?', current_user.email,current_user.id).group('comps.id')
ORIGINAL:
My model associations are:
Comp.rb
has_many :teams
has_many :members, :through => :teams
Team.rb
belongs_to :comp
has_many :members
Member.rb
belongs_to :team
I want to write a query that finds all of the Comps where comps.user_id equals a particular value OR members.member_email equals a particular value for any of the members of that Comp.
I unsuccessfully tried this:
#my_comps = Comp.joins(:members).where('members.member_email = ? OR comps.user_id = ?', email, id)
There are 2 issues with the results returned: 1) it returns duplicate Comps where member_email is equal to the condition and 2) it does NOT return the Comps where the user_id is equal to the condition. I solved problem 1 by adding .group('id') to the end of this code but I feel like there is likely a better way to do it, and more importantly it doesn't solve problem 2.
Any advice on how to approach this differently? Thanks so much.

changed the suggestion, didn't know that .join only uses "INNER JOIN" in newer Rails (having an old version).
The final suggestions was: use .include instead of .join

Related

Find records where association didnt already exists

how to get only records that isn't associated in Ebook model? Simply, i want to offer only ebooks that user didnt already have. I tried much solutions, one of best is:
Magazine.left_joins(:ebooks).where(ebooks: { id: nil })
but i dont know where to specify user_id
model Magazine
has_many :ebooks
has_many :users, :through => :ebook
model Ebook
belongs_to :user
belongs_to :magazine
model User
has_many :ebooks
Im new in rails, sorry for stupid question maybe.
#Hass
thank you, but this generate sql like this:
SELECT "magazines".* FROM "magazines"
INNER JOIN "ebooks" ON "magazines"."id" = "ebooks"."magazine_id"
LEFT OUTER JOIN "ebooks" "ebooks_magazines" ON "ebooks_magazines"."magazine_id" = "magazines"."id"
WHERE "ebooks"."user_id" = 1 AND "ebooks"."id" IS NULL
(WHERE "ebooks"."user_id" = 1 => this returns 0 records, because where filter doesnt find any row in table ebooks)
but i need something like this:
SELECT "magazines".*, ebooks.user_id FROM "magazines"
LEFT JOIN "ebooks" ON "magazines"."id" = "ebooks"."magazine_id" AND ebooks.user_id = 1
LEFT OUTER JOIN "ebooks" "ebooks_magazines" ON "ebooks_magazines"."magazine_id" = "magazines"."id"
WHERE ebooks.user_id IS NULL
this returns rows that im looking for but i dont know how to do this with rais and associations
You want to use a left_outer_join instead.
Magazine.left_outer_joins(:ebooks).where(ebooks: { id: nil })

Disable Rails STI for certain ActiveRecord queries only

I can disable STI for a complete model but I'd like to just disable it when doing certain queries. Things were working swell in Rails 3.2, but I've upgraded to Rails 4.1.13 and a new thing is happening that's breaking the query.
I have a base class called Person with a fairly complex scope.
class Person < ActiveRecord::Base
include ActiveRecord::Sanitization
has_many :approvals, :dependent => :destroy
has_many :years, through: :approvals
scope :for_current_or_last_year, lambda {
joins(:approvals).merge(Approval.for_current_or_last_year) }
scope :for_current_or_last_year_latest_only, ->{
approvals_sql = for_current_or_last_year.select('person_id AS ap_person_id, MAX(year_id) AS max_year, year_id AS ap_year_id, status_name AS ap_status_name, active AS ap_active, approved AS ap_approved').group(:id).to_sql
approvals_sql = select("*").from("(#{approvals_sql}) AS ap, approvals AS ap2").where('ap2.person_id = ap.ap_person_id AND ap2.year_id = ap.max_year').to_sql
select("people.id, ap_approved, ap_year_id, ap_status_name, ap_active").
joins("JOIN (#{approvals_sql}) filtered_people ON people.id =
filtered_people.person_id").uniq
}
end
And inheriting classes called Member and Staff. The only thing related to this I had to comment out to get my tests to pass with Rails 4. It may be the problem, but uncommenting it hasn't helped in this case.
class Member < Person
#has_many :approvals, :foreign_key => 'person_id', :dependent => :destroy, :class_name => "MemberApproval"
end
The problem happens when I do the query Member.for_current_or_last_year_latest_only
I get the error unknown column 'people.type'
When I look at the SQL, I can see the problem line but I don't know how to remove it or make it work.
Member.for_current_or_last_year_latest_only.to_sql results in.
SELECT DISTINCT people.id, ap_approved, ap_year_id, ap_status_name, ap_active
FROM `people`
JOIN (SELECT * FROM (SELECT person_id AS ap_person_id, MAX(year_id) AS max_year, year_id AS ap_year_id, status_name AS ap_status_name, active AS ap_active, approved AS ap_approved
FROM `people` INNER JOIN `approvals` ON `approvals`.`person_id` = `people`.`id`
WHERE `people`.`type` IN ('Member') AND ((`approvals`.`year_id` = 9 OR `approvals`.`year_id` = 8))
GROUP BY `people`.`id`) AS ap, approvals AS ap2
WHERE `people`.`type` IN ('Member') AND (ap2.person_id = ap.ap_person_id AND ap2.year_id = ap.max_year)) filtered_people ON people.id = filtered_people.person_id
WHERE `people`.`type` IN ('Member')
If I remove people.type IN ('Member') AND from the beginning of the second to last WHERE clause the query runs successfully. And btw, that part isn't in the query generated from the old Rails 3.2 code, neither is the one above it (only the last one matches the Rails 3.2 query). The problem is, that part is being generated from rails Single Table Inheritance I assume, so I can't just delete it from my query. It's not the only place that is getting added into the original query, but that's the only one that is causing it to break.
Does anybody have any idea how I can either disable STI for only certain queries or add something to my query that will make it work? I've tried putting people.type in every one of the SELECT queries to try and make it available but to no avail.
Thanks for taking the time to look at this.
I was apparently making this harder than it really was...I just needed to add unscoped to the front of the two approval_sql sub-queries. Thanks for helping my brain change gears.

Exclude object if one of the has_many related entities has the attribute with value x

I came across about the problem excluding data, if the attribute x of one of the associated data has the value 'a'.
Example:
class Order < ActiveRecord::Base
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :order
validate_presence_of :status
end
The query should return all Orders that don't have an Item with status = 'paid' (status != 'paid').
Because of the 1:n association an Order can have many Items. And one of the Itmes can have the status = 'paid'. These Orders must be excluded from the result of my query even if the order has other items with status different from 'paid'.
How would I solve this problem:
paid_items = Items.where(status: 'paid').pluck(:order_id)
orders_wo_paid = Order.where('id NOT IN (?)', paid_items)
Is there an ActiveRecord solution, that solves this problem in one query.
Or are there other ways to solve this question?
I 'm not looking for ruby solution such as:
Order.select do |order|
!order.items.pluck(:status).include?('paid')
end
thx for ideas and inspirations.
You can do:
Order.where('orders.id NOT IN (?)', Item.where(status: 'paid').select(:order_id))
If you're using Rails 4.x then:
Order.where.not(id: Item.where(status: 'paid').select(:order_id))
The query you are interested in is the following, but creating with activerecord will be hard/no very readable:
SELECT
orders.*
FROM
orders
LEFT JOIN
order_items ON orders.id = order_items.order_id
GROUP BY
order_items.order_id
HAVING
COUNT(DISTINCT order_items.id) = COUNT(DISTINCT order_items.status <> 'paid')
Sorry for the sql indentation, I have no idea which are the conventions for it.
A way (not the best one at all) to it with rails (unfortunately writing sql for the most important parts) would be the following:
Order.group(:order_id).joins("LEFT JOIN order_items ON orders.id = order_items.order_id")
.having("COUNT(DISTINCT order_items.id) = COUNT(DISTINCT order_items.status <> 'paid')")
Of course you can play with AREL to get rid of the hard coded sql, but in my opinion it will not be easier to read.
You can have an example of creating lefts joins in this gist: https://gist.github.com/mildmojo/3724189

Conditional has_many in rails 2

My app has applicants with many question_sections with many questions with many answers.
Here I am returning these for an applicant
#question_sections = QuestionSection.find(
:all, :include => {:questions => :answers},
:conditions => ['answers.application_form_id is NULL OR answers.application_form_id = ?', #application_form.id],
:order => 'question_sections.list_index ASC, questions.list_index ASC'
)
What I'd like to do is return a row even if the answer row is null (ie, a left join on answer) so we can identify questions that have not been answered rather than omitting them entirely (which is what happens currently.)
I think the issue might be that the answer belongs to both the question and the applicant;
class Answer < ActiveRecord::Base
belongs_to :question
belongs_to :application_form, :touch => true
So, is pseudo code I'd like 'belongs_to :application_form IF :application_form is not null' - to retain any potential associations.
While I can write all this with SQL fairly easily, I'd like to let rails handle that and fix the model.
In SQL I want to go from this
FROM `question_sections`
LEFT OUTER JOIN `questions` ON questions.question_section_id = question_sections.id
LEFT OUTER JOIN `answers` ON answers.question_id = questions.id
WHERE ((answers.application_form_id IS NULL
OR answers.application_form_id = 656))
to this
FROM `question_sections`
LEFT OUTER JOIN `questions` ON questions.question_section_id = question_sections.id
LEFT JOIN `answers` ON answers.question_id = questions.id AND answers.application_form_id = 656
// No WHERE
Thanks.
EDIT
What I need, I think, is a lambda on the has_many association. Something like;
has_many :answers_and_null_answers, :whatever => lambda ( a = Answer.find(n); if a.nil? a = Answer.new; )
Obviously, thats just messy pseudo - but is this possible?
EDIT #2
Aha! first_or_create does what I want, but seems you can'd do it on :includes. I'm assuming there is something I can do to the model to allow this?
The solution I went with was to add raw SQL to find() call. Wrote my joins manually and then edited the view to suit the different output.
Not what I would have preferred to do, but it works.

ActiveRelation where statement on child attribute

I have a has_one condition that I'm trying to access but am having a little trouble
Solicitation belongs_to :lead
Lead has_many :solicitations
My first statement grabs all solicitations for a user
#solicitations = current_user.solicitations.includes(:lead)
I can already access the attribute lead.case_type and could just cycle through the relation and put them in their places manually, but I figure their is an easier way.
What I am trying to do is something similar to
#solicitations.where("lead.case_type = ?", "Civil")
I have tried these and receive an unknown column error lead.case_type
Solicitation.all(:conditions => {:lead => {:case_type => 'Civil'}}, :joins => :lead)
The problem is that you are using lead.case_type, but (if you're following Rails' conventions) your table name is leads. This should work:
#solicitations = current_user.solicitations.includes(:lead).where("leads.case_type = ?", "Civil")
You could also use joins for that:
#solicitations = current_user.solicitations.joins(:lead).where("leads.case_type = ?", "Civil")
includes does an outer join, whereas joins does an inner join. Since you're querying the joined table an inner join would be better here.
In where you always have to use the table name (plural), but in includes and joins it depends on the relationship. In this case solicitation belongs to lead, so you have to use :lead (singular). It's a bit confusing, but I hope this clears it up for you.

Resources