Let say I have following Model:
class Strategy < ActiveRecord::Base
belongs_to :user
has_many :snapshots, :class_name => 'Strategy', :foreign_key => 'master_id'
belongs_to :master, :class_name => 'Strategy', :counter_cache => :snapshots_count
scope :master_only, -> { where(:master_id => nil) }
end
So any user can create a snapshot of a master-instance of Strategy
In controller I am getting all "master" instances of Strategy that belongs to current_user
#strategies = current_user.strategies.master_only.includes(:user,:snapshots)
Rails correctly loads strategies and snapshots in two queries but fetches user for each snapshot in separate query thus introducing N+1 issue (N+2 in this particular case):
Strategy Load (0.3ms) SELECT `strategies`.* FROM `strategies` WHERE strategies`.`user_id` = 2 AND `strategies`.`master_id` IS NULL LIMIT 10 OFFSET 0
User Load (0.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (2)
Strategy Load (0.8ms) SELECT `strategies`.* FROM `strategies` WHERE `strategies`.`master_id` IN (56, 8, 55, 1, 58, 57, 24, 22)
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
....
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 5 LIMIT 1
Is there a way to load the snapshot user more effectively?
You can make a slight change to the way you include :user and :snapshots to only generate one query to load all the users that belong to the :snapshots:
#strategies = current_user.strategies.master_only.includes(snapshots: :user)
The generated SQL queries will be something like:
SELECT `strategies`.* FROM `strategies` WHERE strategies`.`user_id` = 2 AND `strategies`.`master_id` IS NULL LIMIT 10 OFFSET 0
SELECT `users`.* FROM `users` WHERE `users`.`id` IN (2)
SELECT `strategies`.* FROM `strategies` WHERE `strategies`.`master_id` IN (56, 8, 55, 1, 58, 57, 24, 22)
SELECT `users`.* FROM `users` WHERE `users`.`id` IN (2,5)
UPDATE
According to the OP, the following resolved the N+1 issue:
#strategies = current_user.strategies.master_only.includes(:user, snapshots: :user)
Related
I have an easy many to many relation and It doesn't work and I cannot understand why. I'm sure that is something so obvious... but..
class Content < ApplicationRecord
has_many :content_brands
has_many :brands, through: :content_brands
end
class ContentBrand < ApplicationRecord
belongs_to :content
belongs_to :brand
end
class Brand < ApplicationRecord
establish_connection Rails.application.config.brands_database_configuration
has_many :content_brands
has_many :contents, through: :content_brands
end
But
irb(main):002:0> Content.first.brands
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERRORE: la relazione "content_brands" non esiste
LINE 1: SELECT "brands".* FROM "brands" INNER JOIN "content_brands"...
^
: SELECT "brands".* FROM "brands" INNER JOIN "content_brands" ON "brands"."id" = "content_brands"."brand_id" WHERE "content_brands"."content_id" = $1 ORDER BY "brands"."name" ASC LIMIT $2
The table exists, I can query it
irb(main):006:0> ContentBrand.first.brand
ContentBrand Load (0.5ms) SELECT "content_brands".* FROM "content_brands" ORDER BY "content_brands"."id" ASC LIMIT $1 [["LIMIT", 1]]
Brand Load (27.4ms) SELECT "brands".* FROM "brands" WHERE "brands"."id" = $1 ORDER BY "brands"."name" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> #<Brand id: 1, name: "Nokia", logo: "nokia.jpeg", created_at: "2016-12-08 15:50:48", updated_at: "2017-02-02 15:51:43", web_site: "http://www.nokia.it">
Why?
I'm getting crazy because the inverse relation works
Brand.first.contents
Brand Load (25.8ms) SELECT "brands".* FROM "brands" ORDER BY "brands"."name" ASC LIMIT $1 [["LIMIT", 1]]
Content Load (0.7ms) SELECT "contents".* FROM "contents" INNER JOIN "content_brands" ON "contents"."id" = "content_brands"."content_id" WHERE "content_brands"."brand_id" = $1 ORDER BY "contents"."published_at" DESC LIMIT $2 [["brand_id", 391], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
irb(main):011:0>
Update: I forgot to tell you that Brand is on another database...
You can't setup associations to a model that is stored in another database in ActiveRecord. Which makes sense since you can't join another database in a single query in Postgres without jumping through some pretty serious hoops (Postgres_FDW). And with the polyglot nature of ActiveRecord this would just be too much complexity for a very limited use case.
If its in any way possible I would switch to a single database setup even if it means that you have to duplicate data.
If you look at the "inverse query" you can see that it works because its not a single query:
# queries the "brands" database
Brand Load (25.8ms) SELECT "brands".* FROM "brands" ORDER BY "brands"."name" ASC LIMIT $1 [["LIMIT", 1]]
# queries your main database
Content Load (0.7ms) SELECT "contents".* FROM "contents" INNER JOIN "content_brands" ON "contents"."id" = "content_brands"."content_id" WHERE "content_brands"."brand_id" = $1 ORDER BY "contents"."published_at" DESC LIMIT $2 [["brand_id", 391], ["LIMIT", 11]]
However this does not mean that the concept is feasible.
I am building an attendance tracker right now. I have three models currently. I am using devise for my User model
class User < ActiveRecord::Base
has_many :clients
has_many :attendances
class Client < ActiveRecord::Base
belongs_to :user
has_many :attendances
class Attendance < ActiveRecord::Base
belongs_to :user
belongs_to :client
The columns on the attendance table are user_id client_id created_at and updated_at
Here's my thought process:
I can get all of the attendances for each User
I can also get all of the attendances for a specific Client
I can get all of the attendance records for a specific User and Client by way of: Attendance.joins(:user, :client).where(:user_id => current_user) which returns an
<ActiveRecord::Relation [#<Attendance id: 18, client_id: 151, created_at: "2015-07-24 21:36:16", updated_at: "2015-07-24 21:36:16", user_id: 4>, #<Attendance id: 19, client_id: 101, created_at: "2015-07-24 21:37:10", updated_at: "2015-07-24 21:37:10", user_id: 4>, #<Attendance id: 20, client_id: 114, created_at: "2015-07-24 21:37:39", updated_at: "2015-07-24 21:37:39", user_id: 4>, #<Attendance id: 21, client_id: 123, created_at: "2015-07-24 21:38:26", updated_at: "2015-07-24 21:38:26", user_id: 4>]>
Can I somehow refer back to Client table to get information like first_name or email with another where, include, or joins statement?
Or am I missing something altogether and maybe need a join table and do a has_many, through: relationship?
Brad's comment above is technically correct, but since the .joins clause on your query only includes those tables for the execution of the actual query, the data from the Users and Clients tables isn't actually going to get loaded in by Rails, so in order to fetch all their data you'll end up executing N+1 queries (a common cause of slow Rails apps!). That is:
irb> User.joins(:groups).each { |u| u.groups.map(&:name) }
User Load (8.6ms) SELECT "users".* FROM "users" INNER JOIN "groups" ON "groups"."user_id" = "users"."id"
Group Load (1.2ms) SELECT "groups".* FROM "groups" WHERE "groups"."user_id" = $1 [["user_id", 1]]
Group Load (0.6ms) SELECT "groups".* FROM "groups" WHERE "groups"."user_id" = $1 [["user_id", 1]]
Group Load (0.7ms) SELECT "groups".* FROM "groups" WHERE "groups"."user_id" = $1 [["user_id", 1]]
Group Load (0.7ms) SELECT "groups".* FROM "groups" WHERE "groups"."user_id" = $1 [["user_id", 2]]
Group Load (0.6ms) SELECT "groups".* FROM "groups" WHERE "groups"."user_id" = $1 [["user_id", 3]]
Not so bad now, but imagine if you had a thousand users! We can fix this, though. If you use the .includes method, you'll both join onto the other table(s) and load their data into memory. It still runs 2 queries, but that's an improvement:
irb(main):016:0> User.includes(:groups).each { |u| u.groups.map(&:name) }
User Load (0.6ms) SELECT "users".* FROM "users"
Group Load (1.2ms) SELECT "groups".* FROM "groups" WHERE "groups"."user_id" IN (1, 2, 3, 4)
So, for your case, try this instead:
Attendance.includes(:user, :client).where(user_id: current_user)
For more info on the difference between includes and joins, see this blog post, or Google "rails joins vs includes".
All examples below would have called both pending_notifications? and reviewable_notifications
I have the following set of instance methods for User:
def pending_notifications?
return true if reviewable_notifications.size > 0
end
def reviewable_notifications
#reviewable_notifications ||= self.employee.notifications.where(read: [nil, false])
end
They are used by the view in the following manner:
<% if current_user.pending_notifications? %>
<li><%= link_to fa_icon("envelope") + " #{current_user.reviewable_notifications.count} Notification(s)", user_notifications_path(id: current_user.id) %></li>
<% else %>
<li><%= link_to fa_icon("inbox") + " Notification Center", user_notifications_path(id: current_user.id) %></li>
<% end %>
When I analyze the load, one query is being called:
SELECT COUNT(*) FROM "notifications" WHERE "notifications"."employee_id" = ? AND (("notifications"."read" = 'f' OR "notifications"."read" IS NULL))
This is fine, but before my refactor, I was not using the recommended technique of caching with an instance variable, but I got the exact same query in my analysis. Furthermore, it was consistently running at about 20ms less. Below is how the code was originally written. Why wasn't Rails calling the same query twice? Why is the performance better with the code written this way?
def pending_notifications?
return true if self.employee.notifications.where(read: [nil, false]).size > 0
end
def reviewable_notifications
self.employee.notifications.where(read: [nil, false])
end
Memoizing a value like this is only useful if there's an expensive calculation made in Ruby and you use that value in multiple places.
This particular calculation happens in SQL and Rails already caches DB queries by default, so that's why you didn't see any change.
The difference is not based on the query that is being generated...
It is that if you use
`#reviewable_notifications ||= self.employee.notifications.where(read: [nil, false])`
you will only hit the database as long as #reviewable_notifications is nil.
In the moment it takes a value, it will be used.
As an easy example you can write this in the console:
2.1.2 :001 > 5.times { User.first } # no caching, hit 5 times your DB
User Load (0.2ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
User Load (0.1ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
User Load (0.1ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
User Load (0.1ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
User Load (0.1ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=> 5
2.1.2 :002 > 5.times { #user ||= User.first } # caching, only 1 hit
User Load (0.1ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=> 5
Of course, mysql hast its own query cache, so if the same query hits the database, it might be possible that the result is given back from the database query cache (you can see in the example above that the last queries takes less time than the first one, which probably is because mysq serves the results from its cache)
I have a lot of cases in my app where a user has no more than one object (say, a "Description") within its association to another object (a "Group").
For example:
class User < ActiveRecord::Base
has_many :descriptions
has_many :groups
class Group < ActiveRecord::Base
has_many :users
has_many :descriptions
class Description < ActiveRecord::Base
belongs_to :user
belongs_to :group
If I wanted to render all the users in certain group and include their relevant descriptions, I could do something this:
#users model
def description_for(group_id)
descriptions.find_by_group_id(group_id)
end
#view
#group.users.each do |user|
user.name
user.description_for(#group.id).content
But this generates a huge number of Description queries. I've tried using joins:
#controller
#group = Group.find(params[:id], :joins => [{:users => :descriptions}], :conditions => ["descriptions.group_id = ?", params[:id]])
But since I'm still calling user.description_for(#group.id) it doesn't help with the page loading.
UPDATE: Sample generated SQL
Rendered users/_title.html.haml (1.6ms)
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 37 LIMIT 1
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = 7 LIMIT 1
CACHE (0.0ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = 28 LIMIT 1
Description Load (0.1ms) SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 7 AND "descriptions"."group_id" = 28 LIMIT 1
CACHE (0.0ms) SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 7 AND "descriptions"."group_id" = 28 LIMIT 1
Rendered users/_title.html.haml (1.7ms)
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 37 LIMIT 1
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 51 LIMIT 1
CACHE (0.0ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = 28 LIMIT 1
Description Load (0.1ms) SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 51 AND "descriptions"."group_id" = 28 LIMIT 1
CACHE (0.0ms) SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 51 AND "descriptions"."group_id" = 28 LIMIT 1
Rendered users/_title.html.haml (1.8ms)
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 37 LIMIT 1
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = 5 LIMIT 1
CACHE (0.0ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = 28 LIMIT 1
Description Load (0.1ms) SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 5 AND "descriptions"."group_id" = 28 LIMIT 1
CACHE (0.0ms) SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 5 AND "descriptions"."group_id" = 28 LIMIT 1
Rendered users/_title.html.haml (1.7ms)
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 37 LIMIT 1
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = 52 LIMIT 1
CACHE (0.0ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = 28 LIMIT 1
Description Load (0.2ms) SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 52 AND "descriptions"."group_id" = 28 LIMIT 1
CACHE (0.0ms) SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 52 AND "descriptions"."group_id" = 28 LIMIT 1
Rendered users/_title.html.haml (1.7ms)
Right, I think that actually you don't need the joins clause in rails 3. If you use include and where, Arel will do the hard work for you.
I've tested this (albeit using a different set of models (and attributes) than yours) using models with the same underlying arrangement of associations, and I think this should work:
in models/user.rb:
scope :with_group_and descriptions, lambda { |group_id| includes(:groups, :descriptions).where(:groups => { :id => group_id }, :descriptions => { :group_id => group_id }) }
Then in your controller you call:
#users = User.with_group_and_descriptions(params[:id])
Finally in the view you can then do:
#users.each do |user|
user.name
user.descriptions.each do |desc|
desc.content
# or
#users.each do |user|
user.name
user.descriptions[0].content
If I've gotten my thinking right then this should only make 2 db calls. One to get a list of user_ids and the second to get the user, group and description data, and even though you're calling a user object's descriptions method, which would ordinarily have all the descriptions in (not just the ones for a particular group), because you've already populated the association rails won't go off an grab all the associations again when you call user.descriptions, instead it'll just list the ones you've pulled from the DB using the descriptions.group_id where clause. Calling user.descriptions(true) however will force a reload of the descriptions leading to it returning an array of all the description associations for a user.
Take a look at include--it specifies an association that should be eager-loaded.
Railscasts #181: Include vs Joins (or the ASCIIcast version)
Ruby on Rails Guides - see section 4.1.2.7
I have two Models called User and Membership.
User has_many :memberships
Membership belongs_to :user
What is the proper way to modify MembershipsController's index method to set #memberships to all the memberships there are for the user "session[:user_id]"?
I tried something like:
#memberships = Membership.find(:all, :conditions => ["user_id = ?", session[:user_id]] )
but then Rails is Selecting from users instead of memberships:
Rendering memberships/index
←[4;35;1mUser Columns (3.0ms)←[0m ←[0mSHOW FIELDS FROM `users`←[0m
←[4;36;1mUser Load (1.0ms)←[0m ←[0;1mSELECT * FROM `users` WHERE (`users`.`id` = 1) LIMIT
←[4;35;1mCACHE (0.0ms)←[0m ←[0mSELECT * FROM `users` WHERE (`users`.`id` = 1) LIMIT 1←[0m
user_id wasn't being set correctly.
script/console:
>> Membership.find(:first, :conditions => "user_id = 1")
=> nil
Logs:
Completed in 19ms (View: 9, DB: 6) | 200 OK [http://localhost/memberships]
[4;36;1mSQL (0.0ms)[0m [0;1mSET NAMES 'utf8'[0m
[4;35;1mSQL (0.0ms)[0m [0mSET SQL_AUTO_IS_NULL=0[0m
[4;36;1mUser Columns (2.0ms)[0m [0;1mSHOW FIELDS FROM `users`[0m
[4;35;1mSQL (0.0ms)[0m [0mSHOW TABLES[0m
[4;36;1mUser Load (0.0ms)[0m [0;1mSELECT * FROM `users` WHERE (`users`.`id` = 1) [0m
[4;35;1mMembership Load (1.0ms)[0m [0mSELECT * FROM `memberships` WHERE (user_id = 1) LIMIT 1[0m
[4;36;1mMembership Load (0.0ms)[0m [0;1mSELECT * FROM `memberships` WHERE (user_id = 1) LIMIT 1[0m
What does session[:user_id] represent? Are you trying to roll your own authentication system?
There are a number of authentication solutions that can handle the ins and outs of that for you.
To answer your question though, you probably want to be making use of the associations you've set up between the two models:
def index
#user = User.find(session[:user_id])
#memberships = #user.memberships if #user
end
Using an application-wide authentication system (whether of your own-making or using a library) this would most likely be simplified to:
def index
#memberships = current_user.memberships
end
where current_user is a method defined in ApplicationController that returns the currently logged in user and the index action has a before_filter that ensures that the user is logged in.