How to eager load associations with the current_user?
Asked Answered
P

4

23

I'm using Devise for authentication in my Rails app. I'd like to eager load some of a users associated models in some of my controllers. Something like this:

class TeamsController < ApplicationController

  def show
    @team = Team.includes(:members).find params[:id]
    current_user.includes(:saved_listings)

    # normal controller stuff
  end
end

How can I achieve this?

Pestana answered 1/8, 2011 at 18:11 Comment(1)
Overwrite the current_user methodMasticatory
C
29

I ran into the same issue and although everyone keeps saying there's no need to do this, I found that there is, just like you. So this works for me:

# in application_controller.rb:
def current_user
  @current_user ||= super && User.includes(:saved_listings).find(@current_user.id)
end

Note that this will load the associations in all controllers. For my use case, that's exactly what I need. If you really want it only in some controllers, you'll have to tweak this some more.

This will also call User.find twice, but with query caching that shouldn't be a problem, and since it prevents a number of additional DB hits, it still is a performance gain.

Crispin answered 15/11, 2011 at 12:5 Comment(4)
I haven't tested it but it looks good to me. Would be easy to make this accept an argument representing an array of table names to include also.Pestana
Note that since Rails 4, this answer using #find does trigger two separate database queries on users. The implementation of #first (used by OrmAdapter::ActiveRecord#get, which Devise calls to serialize its current_user object) in Rails 4+ adds an ORDER BY users.id ASC clause while #find does not, so the two queries no longer match in the query cache. User.includes(:saved_listings).where(id: @current_user.id).first will match across Rails versions.Castoff
+1 for the DB queries above. This can potentially be an expensive solution. Look for a better alternative me and my teammate found in a separate answer using ActiveRecord::Associations::PreloaderGravely
I agree this is a good solution. But for more advanced cases this link might be helpful - blog.widefix.com/parameterized-rails-associationsNatalee
H
17

Override serialize_from_session in your User model.

class User
  devise :database_authenticatable

  def self.serialize_from_session key, salt
    record = where(id: key).eager_load(:saved_listings, roles: :accounts).first
    record if record && record.authenticatable_salt == salt
  end
end

This will however, eager load on all requests.

Hurtless answered 8/6, 2016 at 4:18 Comment(2)
The currently selected answer by Thilo above results in a lot more queries. If the intent is to eager load associated models in current_user to reduce the number of queries, overriding the class method, demonstrated in this answer, is the way to go.Streamliner
A slightly more improved version of this is to use ActiveRecord::Relation#scoping then call super in the scoping block. Like eager_load(:saved_listings, roles: :accounts).scoping { super(key, salt) }.Technicality
G
16

I wanted to add what I think is a better solution. As noted in comments, existing solutions may hit your DB twice with the find request. Instead, we can use ActiveRecord::Associations::Preloader to leverage Rails' work around loading associations:

def current_user
  @current_user ||= super.tap do |user|
    ::ActiveRecord::Associations::Preloader.new.preload(user, :saved_listings)
  end
end

This will re-use the existing model in memory instead of joining and querying the entire table again.

Gravely answered 18/6, 2019 at 19:8 Comment(1)
This is by far the best answer, thanks. You could also make a convenience method to make it explicit for the callee. def current_user_with(includes) @current_user_with ||= current_user.tap do |user| ::ActiveRecord::Associations::Preloader.new.preload(user, includes) end endCapitation
S
-3

Why not do it with default_scope on the model?

like so:

Class User  < ActiveRecord::Base
  ...
  default_scope includes(:saved_listings)
  ...
end
Saprogenic answered 25/3, 2014 at 17:17 Comment(2)
Then every query on User will eager load the saved listings (tests included). This is overkill if you only want to eager load in one or two actions and will have a negative effect on the response time of the application and the speed of the test suite.Pestana
Just a thought... If your current_user belongs to a company as well as all other users in the table and if you constantly refer to the company attributes of the returned users or current_user, wouldn't it make sense to have this? Also, Rails 4.2.1 (don't know about previous versions) does not allow default_scope without a block, so you would have to call default_scope { includes(:company) }Wilhelmstrasse

© 2022 - 2024 — McMap. All rights reserved.