How to enforce strict loading of records?

My understanding is that strict loading should be enabled app-wide when adding self.strict_loading_by_default = true to ApplicationRecord. However I silently still get N+1’s with the example below. Would someone have insight into how to enforce strict loading?

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  self.strict_loading_by_default = true
end

class ModuleSession < ApplicationRecord
  has_many :viewerships, as: :record, dependent: :destroy

  scope :with_viewerships, ->(user) {
    # BTW is there any cleaner way to load the user's viewerships?
    joins("LEFT JOIN viewerships "\
          "ON viewerships.record_type = 'ModuleSession' "\
          "AND viewerships.record_id = module_sessions.id AND "\
          "viewerships.user_id = #{user.id}")
      .includes(:viewerships)
  }
end

class Viewership < ApplicationRecord
  belongs_to :user, inverse_of: :viewerships
  belongs_to :record, polymorphic: true
end

class CourseModulesController < ApplicationController
  def show
    ModuleSession.with_viewerships(current_user).each do |s|
      s.viewerships.find_or_initialize_by(user: current_user)
      # s.viewerships.first_or_initialize(user: current_user)
    end
  end
end

When calling the above controller action, a bunch of SQL “Viewership Load” are triggered, see screenshot from rack-mini-profiler:

The N+1 queries disappear when find_or_initialize_by is replaced with first_or_initialize (I don’t know why, but it’s good news). I would have expected a strict loading error to be raised so that I become aware of this problem and can correct it.

first_or_initialize calls first, which is compatible with includes (since it checks for loaded?.

By contrast, find_or_initialize_by calls find_by which always runs a query.

I agree. I think this a bug, I’ll have a go at a fix.

The original strict loading scope just covered things you can cover with includes, preload, etc. That’s probably why this isn’t included.

1 Like

The strict loading mode cascades down from the parent and helps you catch places where eager loading would prevent N + 1.

>> p = Project.includes(:commits).strict_loading.find_by(id: 'abe99c60-56c4-461e-bd59-63418a719e0d')
>> p.commits.first.post
=> ActiveRecord::StrictLoadingViolationError: Commit is marked as strict_loading and Post cannot be lazily loaded.

The strict_loading mode can also be declared as an option on the association to enforce eager loading of associated records.

class Project < ApplicationRecord
  has_many :commits, strict_loading: true
end

>> p = Project.first
>> p.commits.first
=> ActiveRecord::StrictLoadingViolationError: The commits association is marked as strict_loading and cannot be lazily loaded.

>> p = Project.includes(:commits).first
>> p.commits.first
=> ...

The strict_loading mode, helps in avoiding N + 1 that might be otherwise missed. Since these are now baked into Rails, we have a better and predictable result, unlike some issues that the bullet gem might face.