How to simply join n-to-n association in rails? - sql

I have three tables: users, locations, locations_users, The associations are as follows :
user has_many locations_users
user has_many locations :through => locations_users
location has_many locations_users
location has_many users :through => locations_users
How would I find all the users who are joined to location_id = 5 in one query?
Any help would be appreciated.

You can use LEFT OUTER JOIN. It fetches all data from the left table with matching data from right, if present (if not present, join column is null):
User
.joins('LEFT OUTER JOIN locations_users ON locations_users.user_id = users.id')
.where('locations_users.id IS NULL OR locations_users.location_id = ?', 5)
.all
Hovewer:
this query uses join, which's performance can be poor on big tables (maybe two queries will perform faster - test it!)
for better performance, make sure, that indexes are set on joined columns

I don't know 1 query way but the solution below is efficient.
a = User.where("id NOT IN (?)", LocationUser.pluck("DISTINCT user_id"))
b = LocationUser.where(location_id: 5).pluck("DISTINCT user_id")
result = User.where(id: a+b)

Related

How to implement complex SQL queries in Ruby on Rails?

I have four tables: "users", "user_groups", "groups", "categories".
"users" and "groups" are many_to_many relations through "user_groups".
"groups" and "categories" are many_to_one relations.
I created the following SQL query, but I'm not sure how to implement it in Ruby on Rails:
SELECT u.*
FROM users u
WHERE EXISTS (SELECT 1
FROM user_groups ug,
groups g,
categories c
WHERE u.id = ug.user_id
AND ug.group_id = g.id
AND g.category_id = c.id
AND c.id in ('1, 2, 3'))
What is the best way to implement it without using raw SQL in Ruby on Rails?
I am not a big fan of always translating complex queries into the ActiveRecord query language. Instead, I think it is perfectly fine to write complex queries in plain SQL because SQL is usually easier to write and to understand.
That said, I think this might work:
User.where(
id: UserGroup.select('user_groups.user_id')
.joins(groups: :categories)
.where(categories: { id: [1, 2, 3] })
)
This is probably a lot less complicated than you think:
class User
has_many :user_groups
has_many :groups, through: :user_groups
end
class Group
has_many :user_groups
has_many :users, through: :user_groups
belongs_to :category
end
User.joins(:groups)
.where(groups: { category_id: [1,2,3] })
ActiveRecord will handle joining the intermediate table.
Since this is a left inner join any rows without a match in the join table are omitted. No need to do WHERE EXISTS .... This will give you users that belong to at least one group with the category 1, 2 or 3.

two left outer joins query ambiguous column name

I am making a rails 4 app for a 'question and answer' forum. I have a model Micropost. I have a model QuestionAnswer which connects different microposts as questions and answers:
Class Micropost < ActiveRecord::Base
has_many question_answers, foreign_key: "question_id"
has_many answers, through: :question_answers
has_one :reverse_question_answer, foreign_key: "answer_id", class_name: "QuestionAnswer"
has_one :question, through: :reverse_question_answer
end
Class QuestionAnswer < ActiveRecord::Base
belongs_to :question, class_name: "Micropost"
belongs_to :answer, class_name: "Micropost"
end
Now I want to do a database query. I want to use two LEFT OUTER JOINs to create a joint table with all questions and answers:
Micropost.
joins('LEFT OUTER JOIN question_answers ON question_answers.question_id = microposts.id').
joins('LEFT OUTER JOIN microposts ON microposts.id = question_answers.answer_id')
I receive the error message
SQLite3::SQLException: ambiguous column name: microposts.id SELECT "microposts".* FROM "microposts" LEFT OUTER JOIN question_answers ON question_answers.question_id = microposts.id LEFT OUTER JOIN microposts ON microposts.id = question_answers.answer_id ORDER BY created_at DESC
I replaced micropost with answers in the second joins command above, but it complains there is no such table. How can I do two joins? -Thanks.
The solution is to use the following piece of code:
Micropost.
joins('LEFT OUTER JOIN question_answers a ON a.question_id = microposts.id').
joins('LEFT OUTER JOIN microposts m ON m.id = a.answer_id')
As per your question about aliases, the a alias stands for the whole question_answers table. In this particular example, it's just for convenience. However, the m alias, used for microposts, is not.
Here we define that m is an alias for microposts table to signal that it's a different table than the one you are joining before using a.question_id = microposts.id (here the microposts is the first 'instance' of microposts table, and in the next JOIN you define another instance to which you give an m alias).
Not sure if I explained this clearly, look here:
SELECT e.name, mgr.name
FROM employees e
JOIN employees mgr ON (e.manager_id = mgr.id)
;
Here we have again two instances of employees table - we join rows from both tables to get employee's name and his manager's name. Aliases are required here because otherwise it would be unclear which column is taken from which table.
If you would like to learn more about JOIN, you can read:
http://www.w3schools.com/sql/sql_join.asp
Or (there are further links about JOINs, nicely shown and explained, hope it will be useful to you):
What is the difference between "INNER JOIN" and "OUTER JOIN"?

how to scope order in a HABTM association?

I have two models:
class Doctor < ActiveRecord::Base
has_and_belongs_to_many :branches
end
class Branch < ActiveRecord::Base
has_and_belongs_to_many :doctors
end
I want to order the list of doctors(index action) by the branch name either ASC or Desc. How can I do this?
I don't believe the HABTM association works differently than any other one-to-many would in this context, so you'd use select(), group(), and order(), probably something like this:
#doctors = Doctor.joins(:branches).group('doctors.id').order('max(branches.name)')
Naturally, you'll need to choose how to aggregate this - a Doctor can have many Branches, so you'll have to specify which name to use in the ordering - max() may not be the correct aggregate function for your needs.
Note that this will exclude any Doctor models that don't have any associated Branches, since the default for joins is to use an inner join. If you don't want to do that, you'll probably have to write the joins out manually, so that it uses left joins instead of inner joins, like so:
joins('left join branches_doctors on doctors.id = branches_doctors.doctor_id left join branches on branch.id = branches_doctors.branch_id')
The default name for a HABTM join table is the plural form of both models in alphabetical order (hence branches_doctors, rather than doctors_branches).

Find list of groups where at least one member is part of list (rails, SQL)

I have a simple has_many :through arrangement, as shown below
# employee.rb
class Employee < ActiveRecord::Base
has_many :group_assignments
has_many :groups, through: :group_assignments
# ...
end
# group.rb
class Group < ActiveRecord::Base
has_many :group_assignments
has_many :employees, through: :group_assignments
# ...
end
# group_assignment.rb
class GroupAssignment < ActiveRecord::Base
belongs_to :employee
belongs_to :group
end
I have a list of employees. For that list, I want to grab every group that contains at least one of the employees on that list. How would I accomplish this in a manner that isn't horridly inefficient? I'm newish to Rails and very new at SQL, and I'm pretty at a loss. I'm using SQLite in development and PostgreSQL in production.
For a list of employees named employees_list, this will work:
Group.includes(:employees).where('employees.id' => employees_list.map(&:id))
This is roughly the kind of SQL you will get:
SELECT "groups"."id" AS t0_r0,
"groups"."created_at" AS t0_r1, "groups"."updated_at" AS t0_r2,
"employees"."id" AS t1_r0, "employees"."created_at" AS t1_r1, "employees"."updated_at" AS t1_r2
FROM "groups"
LEFT OUTER JOIN "group_assignments" ON "group_assignments"."group_id" = "groups"."id"
LEFT OUTER JOIN "employees" ON "employees"."id" = "group_assignments"."employee_id"
WHERE "employees"."id" IN (1, 3)
So what is happening is that groups and group_assignments tables are first being joined with a left outer join (matching the group_id column in the group_assignments table to the id column in the groups table), and then employees again with a left outer join (matching employee_id in the group_assignments table to the id column in the employees table).
Then after that we're selecting all rows where 'employees'.'id' (the id of the employee) is in the array of employees in the employee list, which we get by mapping employees_list to their ids using map: employees_list.map(&:id). The map(&:id) here is shorthand for: map { |e| e.id }.
Note that you could use joins instead of includes here, but then you would get duplicates if one employee is a member of multiple groups. Kind of subtle but useful thing to know.
Hope that makes sense!
This is the general idea, but depending on your data, you may need to select distinct.
Group.includes(:group_assignments => :employee).where(:employee => {:id => ?}, #employees.map(&:id))
try
Group.joins(:group_assignments).where("group_assignments.employee_id in (?)", #employees.map(&:id))

Is it possible to define a single SQL query that draws a set of permissible foreign_keys from one table and then uses them to filter another?

Specifically, I'm trying to figure out if it's possible to generate SQL that does what I want to feed into Ruby-on-Rails' find_by_sql method.
Imagine there are Users, who are joined cyclically to other Users by a join table Friendships. Each User has the ability to create Comments.
I'd like a SQL query to return the latest 100 comments created by any friends of a given user so I can display them all in one convenient place for the user to see.
This is tricky, since essentially I'm looking to filter the comments by whether their foreign keys for their author are contained in a set of keys obtained derived from the user's friends' primary keys.
Edit: Clarifying the setup. I'm not exactly sure how to write a schema definition, so I'll describe it in terms of Rails.
class User
has_many :friends, :through => :friendships
has_many :comments
end
class Friendship
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
end
def Comment
has_one :User
end
It's not that tricky, you just use joins. To get just the comments you only need to join the Friendships table and the Comments table, but you probably also want some information from the Users table for the person who wrote the comment.
This would get the last 100 comments from people who are friends with the user with id 42:
select top 100 c.CommentId, c.CommentText, c.PostDate, u.Name
from Friendships f
inner join Users u on u.UserId = f.FriendUserId
inner join Comments c on c.UserId = u.UserId
where f.UserId = 42
order by c.PostDate desc