Conditional has_many in rails 2 - ruby-on-rails

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.

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

Primary key is null when left join is used

When I use this request
#questions = Question.joins("left join answers as a on a.user_id = #{current_user.id} and a.question_id = questions.id").select('questions.*, a.*')
Question.id is null. Does anyone know why? Maybe it needs an alias or something like that.
My schema:
class Answer:
belong_to: User
belongs_to: Question
end
class User:
has_many: answers
end
class Question:
has_one: answer
end
The problem is that the answers table probably also has a column named id, which is shadowing the question's id. Although in the result set one column is called questions.id and the other a.id, active record just looks at the last part ('id')
There's no good way around this that I am aware of other than aliasing problematic columns from the answers table, which unfortunately means explicitly naming all of them (although you can of course automate this to an extent)
This is probably because of the ID columns of two tables conflicting with one another.
Update:
Simply changing the order of select columns will help. So instead of .select("questions.*", "answers.*"), try .select("answers.*", "questions.*").
Old answer:
Try the following:
# Assuming the following:
# Question.table_name == "questions"
# Answer.table_name == "answers"
question_columns = Question.column_names # or %w{questions.*}
answer_columns = Answer.column_names.map { |c| "answer_#{c}" }
columns = question_columns + answer_columns
#questions =
Question.
joins("LEFT OUTER JOIN answers ON (
answers.question_id = questions.id
AND
answers.user_id = #{current_user.id}
)").
select(*columns)
#questions.first.id #=> should return some integer
#questions.first.answer_id #=> may or may not return something
However, unless you absolutely need a LEFT JOIN along with those select-columns, it would be much cleaner to accomplish your task using the following:
class Answer
belongs_to :question
end
class User
has_many :answers
has_many :questions, through: :answers
end
current_user.questions

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}

JOIN statement in Rails with OR condition across 2 tables

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

Find all objects with no associated has_many objects

In my online store, an order is ready to ship if it in the "authorized" state and doesn't already have any associated shipments. Right now I'm doing this:
class Order < ActiveRecord::Base
has_many :shipments, :dependent => :destroy
def self.ready_to_ship
unshipped_orders = Array.new
Order.all(:conditions => 'state = "authorized"', :include => :shipments).each do |o|
unshipped_orders << o if o.shipments.empty?
end
unshipped_orders
end
end
Is there a better way?
In Rails 3 using AREL
Order.includes('shipments').where(['orders.state = ?', 'authorized']).where('shipments.id IS NULL')
You can also query on the association using the normal find syntax:
Order.find(:all, :include => "shipments", :conditions => ["orders.state = ? AND shipments.id IS NULL", "authorized"])
One option is to put a shipment_count on Order, where it will be automatically updated with the number of shipments you attach to it. Then you just
Order.all(:conditions => [:state => "authorized", :shipment_count => 0])
Alternatively, you can get your hands dirty with some SQL:
Order.find_by_sql("SELECT * FROM
(SELECT orders.*, count(shipments) AS shipment_count FROM orders
LEFT JOIN shipments ON orders.id = shipments.order_id
WHERE orders.status = 'authorized' GROUP BY orders.id)
AS order WHERE shipment_count = 0")
Test that prior to using it, as SQL isn't exactly my bag, but I think it's close to right. I got it to work for similar arrangements of objects on my production DB, which is MySQL.
Note that if you don't have an index on orders.status I'd strongly advise it!
What the query does: the subquery grabs all the order counts for all orders which are in authorized status. The outer query filters that list down to only the ones which have shipment counts equal to zero.
There's probably another way you could do it, a little counterintuitively:
"SELECT DISTINCT orders.* FROM orders
LEFT JOIN shipments ON orders.id = shipments.order_id
WHERE orders.status = 'authorized' AND shipments.id IS NULL"
Grab all orders which are authorized and don't have an entry in the shipments table ;)
This is going to work just fine if you're using Rails 6.1 or newer:
Order.where(state: 'authorized').where.missing(:shipments)

Resources