SQL query with multiple joins using Rails models as reference - sql

I want to select a count of all surveys where the survey.property.address.city == "Garrison". I have the following models:
Survey
many_to_one :property
Property
one_to_many :surveys
many_to_one :address
Address
one_to_many :properties
How do I query using SQL?
SELECT count(*) FROM surveys JOIN...

Assuming that your table is named like rails would name those objects and you have the foreign keys that are implied by your relations:
SELECT
COUNT(*)
FROM
surveys
JOIN
properties ON surveys.property_id = properties.id
JOIN
addresses ON addresses.id = properties.address_id
WHERE
addresses.city = 'Garrison'
Also your relations are strangely defined... I'm assuming that that is just a psuedocode version to express the relations.
edit: I corrected the second join, because I believe I had the relations backwards.

Related

ActiveRecord query on many-to-many self join table

I have a many-to-many self join table called people that uses the following model:
class Person < ApplicationRecord
has_and_belongs_to_many :children,
class_name: "Person",
join_table: "children_parents",
foreign_key: "parent_id",
association_foreign_key: "child_id",
optional: true
has_and_belongs_to_many :parents,
class_name: "Person",
join_table: "children_parents",
foreign_key: "child_id",
association_foreign_key: "parent_id",
optional: true
end
If it isn't apparent in the above model - in addition to the people table in the database, there is also a children_parents join table with two foreign key index fields child_id and parent_id. This allows us to represent the many-to-many relationship between children and parents.
I want to query for siblings of a person, so I added the following method to the Person model:
def siblings
self.parents.map do |parent|
parent.children.reject { |child| child.id == self.id }
end.flatten.uniq
end
However, this makes three SQL queries:
Person Load (1.0ms) SELECT "people".* FROM "people" INNER JOIN "children_parents" ON "people"."id" = "children_parents"."parent_id" WHERE "children_parents"."child_id" = $1 [["child_id", 3]]
Person Load (0.4ms) SELECT "people".* FROM "people" INNER JOIN "children_parents" ON "people"."id" = "children_parents"."child_id" WHERE "children_parents"."parent_id" = $1 [["parent_id", 1]]
Person Load (0.4ms) SELECT "people".* FROM "people" INNER JOIN "children_parents" ON "people"."id" = "children_parents"."child_id" WHERE "children_parents"."parent_id" = $1 [["parent_id", 2]]
I know that it is possible to make this a single SQL query like so:
SELECT DISTINCT(p.*) FROM people p
INNER JOIN children_parents cp ON p.id = cp.child_id
WHERE cp.parent_id IN ($1, $2)
AND cp.child_id != $3
$1 and $2 are the parent ids of the person, and $3 is the person id.
Is there a way to do this query using ActiveRecord?
You can use something like this:
def siblings
Person.select('siblings.*').from('people AS siblings').where.not(id: id)
.where(
parents.joins(
'JOIN children_parents ON parent_id = people.id AND child_id = siblings.id'
).exists
)
end
Here you can see few strange things:
from to set table alias. And you should avoid this, because after such table aliasing active record will not help any more with column names from ruby: where(column: value).order(:column) - will not work, only plain sql strings are left
exists - I use it very often instead of joins. When you are joining many records to one, you are receiving duplicates, then comes distinct or group and new problems with them. Exists also gives isolation of query: table and columns in EXISTS expression are invisible for other parts of query. Bad part of using it in rails: at least 1 plain SQL is needed.
One weakness of this method: if you will call it for each record somewhere, then you will have 1 query for each record - N+1 problem.
Now few words about The Rails Way. Rails guide suggests to always use has_many :through instead of habtm, I seen it here: https://github.com/rubocop-hq/rails-style-guide.
Rails ideology as I understood it stands for speed of development and simplicity of maintenance. First means that performance does not matter (just imagine how many users you need to start have issues with it), second says that flexibility of plain SQL is good, but not in rails, in rails please make code as simple as possible (see rubocop defaults: 10 loc in method, 100 loc in class, and 4 complexity metrics always saying that your code is too complex). I mean, many real world rails projects are making queries with N+1, making ineffective queries, and this rarely becomes a problem
So, in such cases I would recommend to try includes, preload, eager_load.

Querying between two tables that share an association

New to seqeul and sql in general. I have two tables, groups and resources, that are associated many_to_many and therefore have a groups_resources join table. I also have a task table that has a foreign_key :group_id, :groups and is associated many_to_one with groups.
I'm trying to figure out what query to use that will allow my to get the resources that are able to do a task, based on a task's group. Do I have to do a complicated query via the `groups_resources' join table, or is there a more straightforward query/ way of setting up my associations?
Thanks!
I would structure the SQL statement as below. Which would provide you the resources objects that are associated with a specific task id through the join table.
SELECT r.*
FROM resources r
JOIN groups_resources gr ON gr.resources_id = r.id
JOIN groups g ON gr.group_id = g.id
JOIN task t ON t.id = g.id
WHERE t.id = ?
I think following is enough:
select res.* from resources res, task tk, groups_resources gr
where res.resource_id = gr.resource_id and
gr.group_id = tk.group_id and
tk.group_id=<>;
The other two answers are helpful for how to structure a SQL query, but thought I would answer my own question specifically as it relates to Sequel. Turns out there is a many_through_many plugin that makes this sort of querying simple, if you make both tables many_to_many :
Task.plugin :many_through_many
Task.many_through_many :resources,
:through =>[
[:groups_tasks, :task_id, :group_id],
[:groups, :id, :id],
[:groups_resources, :group_id, :resource_id]
]
Now you can just call something like task.resources on a Task instance, even though your tables don't explicitly associate tasks and resources.

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

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)

how to perform additional inner join on pivot table with laravel eloquent

I have four tables:
foods: id, name
users: id, name
mealtypes: id, name
food_user: id, food_id, user_id, mealtype_id
foods and user have a many-to-many relationship
mealtype has a one-to-one relationship with food_user
In the end I would like to have an instance of a model with the following properties:
food.name, users.name, mealtype.name
normal sql would be:
SELECT f.name, u.name, m.name FROM foods f
INNER JOIN food_user fu ON f.id = fu.food_id
INNER JOIN users u ON fu.id = u.id
INNER JOIN mealtypes m ON fu.mealtype_id = m.id
Thanks for any help!
You could do something like this with Eloquent and Query Builder, assuming you have a model named Food:
$foods = Food::join('food_user', 'foods.id', '=', 'food_user.food_id')
->join('users', 'food_user.user_id', '=', 'users.id')
->join('mealtypes', 'food_user. mealtype_id', '=', 'mealtypes.id')
->get();
There's a good documentation about the query builder too: http://www.laravel.com/docs/queries
To answer my own question a year later. I actually asked the wrong question. A pivot table is just a many-to-many relationship between two tables. If a table that represents this many-to-many relationship additionally relates to other tables it is not a pivot table. So in my case the table food_user should represent an eloquent entity on its own with the three relationships defined.
Update, generic solution:
I cannot give you my final solution in terms of Eloquent, since I haven't used it in ages (and didn't implement it there), so I do not have the knowledge anymore. In more general terms, one would need to create 4 models:
Food
User
MealType
Meal (instead of FoodUser, don't like that name)
Now, in the model Meal, you need to define your relations with the other models:
Meal.food has a many to one relation with Food
Meal.user has a many to one relation with User
Meal.mealType has a many to one relation with MealType
Some other properties such as Meal.calories (int) and Meal.date (DateTime)

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