rails: StiPreload implementation from Rails Guides results in uninitialized constant errors on Rails 7

When using the StiPreload module implementation suggested in the Rails Guides it results in NameError: uninitialized constant for typical use cases with existing STI type data.

I believe the fact that this issue pops up with Rails 7 stems from the changes here: https://github.com/rails/rails/commit/ffae3bd8d69f9ed1ae185e960d7a38ec17118a4d .

Steps to reproduce

I’ve added a repo to demonstrate the problem here: @dwillett/rails_7_sti_preload

$ bin/rails db:create
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
$ bin/rails db:migrate
== 20220124235533 AddUsers: migrating =========================================
-- create_table(:users)
   -> 0.0022s
== 20220124235533 AddUsers: migrated (0.0023s) ================================

$ bin/rails r "Member.create(name: 'Foo')"
$ bin/rails r "puts Member"
Member
$ SHOWCASE_BUG=true bin/rails r "puts Member"
Please specify a valid ruby command or the path of a script to run.
Run 'rails runner -h' for help.

uninitialized constant Member

When the environment variable is present, self.descendants will be invoked as part of requiring the User base STI class. It appears that invoking Class.descendants while the class is being loaded has implicitly been a circular problem with the StiPreload module (In this example: Member autoloads User, which invokes .descendants which tries to constantize Member), but with the changes in Rails 7 it now invokes that method directly when setting up association callbacks instead of ActiveSupport::DescendantsTracker.descendants(self).

Expected behavior

Class should be autoloaded correctly without error.

Actual behavior

Class encounters unitialized constant error when attempting to autoload and preload class.

System configuration

Rails version: 7.0.1

Ruby version: 3.0.3

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 7
  • Comments: 18 (14 by maintainers)

Most upvoted comments

Here’s the updated module with the proper (private API) hook:

module StiPreload
  unless Rails.application.config.eager_load
    extend ActiveSupport::Concern

    class_methods do
      def type_condition(...)
        unless @_sti_preloaded
          types_in_db = base_class.unscoped.select(inheritance_column).distinct.pluck(inheritance_column).compact
          @_sti_preloaded = types_in_db.map { |type| type.safe_constantize }.all?
        end
        super
      end
    end
  end
end

It only aims at solving the main issue which is querying. e.g. if you have User > Member > VIPMember, then in autoloading mode, Member.all don’t include VIPMember records unless VIPMember is already load.

This new module does fix this issue, which is pretty much we wanted to do in https://github.com/rails/rails/pull/36487

Note that it doesn’t try to make descendants or subclasses behave like they would in an eager loaded environment, as I don’t think it’s as much of a problem.