21

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?

David Tuite
  • 22,258
  • 25
  • 106
  • 176

4 Answers4

28

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.

Thilo
  • 17,565
  • 5
  • 68
  • 84
  • 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. – David Tuite Nov 17 '11 at 05:41
  • 1
    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`](https://github.com/ianwhite/orm_adapter/blob/v0.5.0/lib/orm_adapter/adapters/active_record.rb#L15-L18), 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. – wjordan Oct 18 '17 at 17:01
  • 1
    +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::Preloader` – xHocquet Jun 18 '19 at 19:05
  • I agree this is a good solution. But for more advanced cases this link might be helpful - https://blog.widefix.com/parameterized-rails-associations/ – ka8725 Mar 29 '23 at 11:40
15

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.

Vikrant Chaudhary
  • 11,089
  • 10
  • 53
  • 68
  • 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. – ybakos May 09 '18 at 14:06
15

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.

xHocquet
  • 362
  • 2
  • 11
  • 2
    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 end – Ryan Romanchuk Sep 28 '19 at 18:52
-3

Why not do it with default_scope on the model?

like so:

Class User  < ActiveRecord::Base
  ...
  default_scope includes(:saved_listings)
  ...
end
bmac
  • 316
  • 3
  • 6
  • 3
    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. – David Tuite Mar 26 '14 at 14:03
  • 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) }` – kobaltz May 10 '15 at 04:23