So I have 2 tables that are joined by an ID. I'm in rails console and I type:
Programmer.all(:joins=>:assignment)
the sql that is generated is:
SELECT `programmers`.* FROM `programmers` INNER JOIN `assignments` ON `assignments`.`programmer_id` = `programmers`.`id`
The output that is generated is the same as Programmer.all. Why doesn't it include the assignments data?
I believe I majorly overanalyzed your question. If you just want to join any available assignments to programmers, you're looking for:
Programmer.all(:include => :assignment)
Rails is designed so that :joins is used to perform things like sorting and grabbing certain records but still keep the query result to a minimum size -- meaning, :joins never actually includes the results from the joined table in the result.
Now here's my previous answer that assumes you want to perform an INNER JOIN to get only the programmers with assignments, but you also want that data. In that case, you have two options:
#1 - Use :select
Programmer.all(:select => '*', :joins => :assignment)
That will change the SQL to:
SELECT * FROM `programmers` INNER JOIN `assignments` ON `assignments`.`programmer_id` = `programmers`.`id`
Upside: You get the query you want and all the data is somewhere, at least.
Downside: assignments is assigned directly to the Programmer object and not to the proper place at Programmer.assignment.
#2 - Use a combination of :joins and :includes
Programmer.all(:joins => :assignment, :include => :assignment)
Which produces the SQL:
SELECT `programmers`.* FROM `programmers` INNER JOIN `assignments` ON `assignments`.`id` = `programmers`.`assignment_id`
SELECT `assignments`.* FROM `assignments` WHERE (`assignments`.`id` IN (?) )
Upside: All your data is in the right place now. You can refer to programmer.assignment without another query.
Downside: You are running that extra query in a lot of instances. I am fairly sure that Rails tries to optimize this when it needs to, though, and if not, it shouldn't cause you too much overhead.
Simply you can do like
Programmer.includes(:assignment)
Related
I am using the following Ruby code:
sql = "SELECT * FROM
conversations
INNER JOIN member_users ON member_users.conversation_id = conversations.id
WHERE conversations.id = #{conversation.id}
AND member_users.user_id = #{user.id}"
cs = Conversation.find_by_sql(sql)
The query returns a single row. But, find_by_sql is returning the Conversation with the ID of the member_user, not the ID of the Conversation. This is because there are two "id" columns in the result, and find_by_sql seems to be using the wrong one.
Is there any way to prevent this without using SELECT conversations.* instead of SELECT *?
Why does this happen - why doesn't Rails use the first ID column it comes across? Is it because the row is returned in a hash?
More generally, in SQL - is there a way to differentiate between different columns with the same name? Or are these meant to be equivalent? I often get confused when doing joins which result in several "id" cols.
Thanks,
Louise
If you do a join in a find_by_sql query, ActiveRecord sometimes mixes up identically-named columns from the tables. You can hint ActiveRecord to correctly map by changing the SELECT clause to only include the table columns you are interested in.
So to fix your problem, change your query to begin with
"SELECT conversations.* FROM conversations ..."
I would find it like so
class MemberUser < ActiveRecord::Base
belongs_to :conversation_user
scope :users, ->(*u) {
where(user_id: u.flatten.compact.uniq)
}
scope :conversations, -> (*c) {
where(conversation_id: c.flatten.compact.uniq)
}
end
Conversation.joins(:member_users).merge(MemberUser.users(user))
As for your direct question, why are you trying to find the conversation when you already have it?
I have two Models invoice and payments. The relationship between them is invoice has_many payments.
I'm using the following left outer join to return all invoices that have not been paid at all:
result1 = Invoice.includes(:payments).where(:payments => { :id => nil })
I'm also interested in all invoices that have been partially paid. To return those I use an inner join:
result2 = Invoice.joins(:payments).group("transfers.id").having("sum(payments.amount) < invoice.amount")
I would now like to combine the two results, i.e. I want all Invoices that have either not been paid, or not been fully paid. I know that I could just do result = result1 + result2. However, this doesn't return an ActiveRecord object. Is there a way to combine those two queries in a single query?
I use Rails 4.1 and PostgreSQL
I believe you're correct that you can't get ActiveRecord to generate anything other than an inner join without writing it yourself. But I wouldn't use includes in this context, for while it does happen to cause ActiveRecord to generate a different join, that is an "implementation detail" -- it's fundamental purpose is to load the associated models, which is not necessarily what you want.
In your proposed solution, I don't understand why you'd group by both invoices.id and payments.id -- that seems to defeat the purpose of grouping.
You could do something like the following, although I can't say that this seems much more Rails-ish.
Invoice.joins("LEFT JOIN payments ON payments.transfer_id = invoices.id")
.select("invoices.id, SUM(payments.amount) AS total_paid")
.group("invoices.id")
.having("SUM(payments.amount) IS NULL OR SUM(payments.amount) < invoices.amount")
This will return a list of Invoice objects with only the id field and total_paid field set. If you need other fields available, add them to both the select statement and the the group statement.
I ended up doing just a LEFT OUTER JOIN. However, Rails doesn't seem to support group and having with the LEFT OUTER JOIN generated by includes(). Therefore, i had to construct the join manually which actually doesn't feel like "the Rails way".
My query:
Invoice.joins("LEFT JOIN payments ON payments.transfer_id = invoices.id").group("invoices.id, payments.id").having("((payments.id IS NULL) OR (sum(payments.amount) < invoices.amount))")
Update: This doesn't work as expected. Please see the accepted answer.
Coming from django, we have something called select_related that does a join when executing a query such that related objects' data are also fetched.
e.g.
# rails + select_related
p = Person.where(job: 1).select_related("job_name")
# so the return query list has objects that
# can call person.job.job_name without another query
# because selected_related did a join on jobs table
How do you do this in rails/activerecord?
In rails, it's more common to use includes to handle join tables. It can either do a left outer join (when a where condition needs to reference the joined table) or one more query such as select * from jobs where id IN (1,3,4,5) which solves the n+1 optimization problem.
In your case I would:
p = Person.where(job: 1).includes(:jobs)
job = p.job.job_name
This does still use two queries, but this is not the use case it is optimized for (and this case doesn't deserve optimization) but if you had a more complicated case it gets better:
people = Person.where(status: 'active').includes(:jobs)
people.each {|p| puts p.job.job_name}
In this case, it will still only execute 2 queries.
I'm looking to write an ActiveRecord query and this is what I have below. Unfortunately you can't use OR like this. What's the best way to execute? category_ids is an array of integers.
.where(:"categories.id" => category_ids).or.where(:"category_relationships.category_id" => category_ids)
One way is to revert to raw sql...
YourModel.where("categories.id IN ? OR category_relationships.category_id IN ?", category_ids, category_ids)
Keep the SQL out of it and use ARel, like this:
.where(Category.arel_table[:id].in(category_ids).
or(CategoryRelationship.arel_table[:category_id].in(category_ids))
Assuming you want to return Categories, you need to OUTER JOIN category_relationships and put a OR condition on the combined table.
Category.includes(:category_relationships).where("categories.id IN (?) OR category_relationships.category_id IN (?)",category_ids,category_ids )
This query is creating an outer join table by combining columns of categories and category_relationships. Unlike an inner join (e.g. Category.joins(:category_relationships)), outer join table would also have categories with no associated category_relationship. It would then apply the conditions in whereclause on the outer join table to return the matching records.
includes statement without conditions on the association usually makes two separate sql queries to retrieve the records and their association. However when used with conditions on the associated table, it would make a single query to create an outer join table and run conditions on the outer join table. This allows you to retrieve records with no association as well.
See this for a detailed explanation.
What you want to do is manually write the OR part of the query like this:
.where("category.id in (#{category_ids.join(',')}) OR category_relationships.category_id in (#{category_ids.join(',')})")
I have three tables:
Users
Questions
User_Questions
1.user questions has the columns user_id, question_id, and answer
I want to find a random question that has not been answered and thus does not have a row in the user_questions table.
If every question has been answered, then return any random question.
I'm told this can be done with an OUTER JOIN, but I'm a SQL noob and I'm not sure how to do that in Rails.
This is what I have:
def next_question
q = Question.all - Question.joins(:user_questions).where
(user_questions: { user_id: user_id })
q = Question.all if q.empty?
return q[rand(q.size)]
end
There's hardly ever a good reason for calling the all method on a model class. This loads every single record in the database of that type into memory, and unless you're absolutely certain this is a small set of records, you could potentially hang your entire system. Even then it's extremely bad form to load in everything and then cherry pick one thing and discard the rest. It's like ordering one of every single item from Amazon, picking out the pen you want, and throwing the rest of the delivery in the trash.
What you probably want is something where you randomly select one record that has not been assigned yet. That probably looks something like this:
Question.where('id NOT IN (SELECT question_id FROM user_questions WHERE user_id=?)', user_id).order('RAND()').first
The problem with a JOIN is that you're going to find records that have matches in the user_questions table and not the inverse.
This query presumes that the number of questions answered by a user is relatively small or that NOT IN could get dramatically more expensive.
Yes, you can use a LEFT OUTER JOIN for this. A normal INNER JOIN will only include rows that match the join condition, a LEFT JOIN will include matching rows and will flesh out the unmatched rows by putting NULLs in all the columns (the PostgreSQL docs have a reasonable description).
So, you do a LEFT JOIN and then look for an unmatched row by looking for a NULL. The SQL would look sort of like this:
select ...
from questions
left outer join user_questions on questions.id = user_questions.question_id
where user_questions.question_id is null
That would give you all the questions that haven't been answered. In ActiveRecord, you could do something like this:
Question.joins('left outer join user_questions on question.id = user_questions.question_id')
.where(:user_questions => { :question_id => nil })
.limit(1)
You might want to play with random ordering as well but that should get you started. If that doesn't give you a match then you can grab a random question with something like this:
n = Questions.count
q = Questions.offset(rand(n)).limit(1)
You could do the above with Questions.order('random()').limit(1) as well but ORDER BY random() can be unpleasant with a large database; getting the count should be pretty fast so doing the random picking in Ruby should be quick enough and it won't make your database hate you.
Also have a look at the documentation for ways to tidy up that joins, I'm working with a database with a slightly non-standard structure so I have to do things the long way sometimes; ActiveRecord is refusing to do a LEFT JOIN for me unless I spell it out, YMMV.