Search code examples
ruby-on-railsactiverecordruby-on-rails-5polymorphic-associations

How can I eager load and join on a condition for a polymorphic?


Here are my models:

class Team < ApplicationRecord
  has_many :team_permissions
end

class TeamPermission < ApplicationRecord
  belongs_to :team
  belongs_to :permissible, polymorphic: true
end

class User < ApplicationRecord
  has_many :team_permissions, as: :permissible
end

I understand you can solve your N+1 problem with includes like so:

Team.includes(team_permissions: :permissible)

Now, I want to only join the permissions under a condition. For example, if they do not belong to a group of ids, so I would expect this to work, but it throws an error.

ActiveRecord:

Team.includes(team_permissions: :permissible).where.not(team_permissions: { id: team_permission_ids })

Error:

ActionView::Template::Error (Cannot eagerly load the polymorphic association :permissible):

Playing around with it further, I found the following worked the way I want it to, but it does not solve the N+1 issue.

Team.includes(:team_permissions).where.not(team_permissions: { id: team_permission_ids })

How could I include eager loading for the .includes with a condition?


Solution

  • Unfortunately Active Record isn't smart enough (nor, to be honest, trusting enough) to work out that it needs to join the first table to apply your condition, but not the second.

    You should be able to help it out by being slightly more explicit:

    Team.
      includes(:team_permissions).   # or eager_load(:team_permissions).
      preload(team_permissions: :permissible).
      where.not(team_permissions: { id: team_permission_ids }
    

    When there are no conditions referencing includes tables, the default behaviour is to use preload, which handles the N+1 by doing a single additional query, and is compatible with polymorphic associations. When such a condition is found, however, all the includes are converted to eager_load, which does a LEFT JOIN in the main query (and is consequently incompatible: can't write a query that joins to tables we don't even know about yet).

    Above, I've separated the part we definitely want loaded via preload, so it should do the right thing.