Count has_many records of query - sql

If I have a Model with a has_many relationship, how can I retrieve all of the records that all of the records in my query point to?
Let's just say, buildings have a has_many relationship with rooms. Here's what I want to do:
Building.where(...query...).rooms.count
This is just an example. I might want to count them, or I might want an ActiveRecord of the rooms that belong to the buildings that match the query.
One way is this, but I'm wondering if there's a better way:
building_ids = Building.where(...query...).pluck(:id)
Room.where(building_id: building_ids).count

Using select instead of pluck will result in a single sql statement instead of two separate ones.
building_ids = Building.where(...).ids
Room.where(building_id: building_ids)
you can also use join
Room.joins(:building).where(building: { name: 'somename' })

I'd use the sum of the counter caches.
Add the counter cache colum in a migration:
add_column :buildings, :rooms_count, default: 0
Update the relation in Room:
belongs_to :building, counter_cache: true
Then you could do something like:
Building.sum(:rooms_count)
That'll avoid n+1 queries
More details here https://blog.appsignal.com/2018/06/19/activerecords-counter-cache.html

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.

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

Include has_many records in query with Rails

I have a has_many (through accounts) association between the model User and the model Player.
I would like to know what is the best way to query all the users, and for each returned record get the associated players usernames attributes (in comma separated values).
So, if for example, a User named 'John' is associated with 3 players with usernames 'john_black', 'john_white', 'john_yellow'. I would like the query to return not just the User attributes but also an attribute called, for example, player_username, that would have this value: john_black, john_white, john_yellow.
I have tried the following Arel query in the User model:
select(`users`.*).select("GROUP_CONCAT(`players`.`username` SEPARATOR ',') AS comma_username").joins(:user)
.joins(:accounts => :player )
Which gives me the following SQL:
SELECT `users`.*, GROUP_CONCAT(`players`.`username` SEPARATOR ',') AS comma_username FROM `users` INNER JOIN `accounts` ON `accounts`.`user_id` = `users`.`id` INNER JOIN `players` ON `players`.`id` = `accounts`.`player_id`
If I execute this in MySQL console it works, but if I try to fetch from the model, I don't get the comma separated values.
What am I missing?
I believe ActiveRecord maps all columns retrieved by the SQL query with all attributes you have on your model, which in most of the cases are exactly the same. Maybe if you create a virtual accessor on your model, ActiveRecord could map your virtual column to the virtual attribute.
class User
attr_accessible :player_username
...
Give it a try and see if this works.

Rails3 and ActiveRecord with legacy database: JOIN not returning other table columns

I have a legacy database that contains two different tables (tbl_players and tbl_player_ratings) that link to one another on a common column (player_key).
My problem: With Rails3, when I attempt to retrieve PlayersRatings joined to Players, only columns from PlayerRatings are returned. However, if I execute the same SQL query at the mysql command line, columns from both tables are returned.
For simplicity, I here provide just a few of the columns from each table.
tbl_players
player_key, last_name
tbl_player_ratings
player_key, rating
My Rails classes representing these tables look as follows:
class PlayerRating < ActiveRecord::Base
establish_connection :legacy
set_table_name 'tbl_player_ratings'
set_primary_key "player_key"
belongs_to :player,
:foreign_key => 'player_key'
end
class Player < ActiveRecord::Base
establish_connection :legacy
set_table_name 'tbl_players'
set_primary_key "player_key"
has_many :player_ratings,
:foreign_key => 'player_key'
end
The query that I'm running in the rails console:
PlayerRating.joins(:player).select("*").limit(1)
This returns a sole PlayerRating without any Player fields represented.
The SQL produced by the above rails command:
SELECT * FROM `tbl_player_ratings` INNER JOIN `tbl_players` ON `tbl_players`.`player_key` = `tbl_player_ratings`.`player_key` LIMIT 1
When I execute that exact command at the mysql command-line, all columns in both tables are returned.
Why does Rails and ActiveRecord not do the same (return all columns in both tables)?
A PlayerRating is only a PlayerRating object; you cannot coerce it into being a combination PlayerRating-Player via a joins(:player) on the query.
Following that query, you will have access to the player via player_rating.player.
array_of_player_ratings = PlayerRating.joins(:player).limit(any_number)
single_player_rating = PlayerRating.joins(:player).first
player = single_player_rating.player
Do look at the duplicate question linked for a fuller description of the difference between SQL queries and ActiveRecord queries.