简体   繁体   中英

Ruby/Rails: Circular dependency when including concern in ApplicationRecord

I have a concern that creates a class macro that I want available for all the models in my Rails application. So I'm including it in ApplicationRecord. The code is as follows:

# application_record.rb

class ApplicationRecord < ActiveRecord::Base

  include ::TestConcern

end

# app/concerns/test_concern.rb

module TestConcern

  extend ActiveSupport::Concern

  class_methods do

    def some_class_macro_all_models_must_have

      User.some_class_instance_variable << self

    end

  end

  included do
    User.include(UserModule)
  end

  module UserModule

    def self.included(base)
      base.class_eval do

        def self.some_class_instance_variable
          @some_class_instance_variable ||= Set.new
        end

      end
    end

  end

end

As you can see, the class macro will actually interact with a class instance variable in the model User.

So that's why, on the included hook of the concern, I'm trying to class_eval the User model to have that class instance variable initialized. The plan was to do it like this because otherwise any model can be invoking the class macro BEFORE the class instance variable is initialized in the User model.

However, this errors out with Circular dependency detected while autoloading constant User . As far as I can understand, ApplicationRecord loads, it includes the module, the module included hooks is called, it references the User model, and so the User model is loaded, which inherits from ApplicationRecord (which didn't finish loading yet), so it causes the circular dependency.

How to avoid this circular dependency paradox, knowing that many models will invoke this class macro, and those classes might be loaded before the User class itself, so I can't even count on defining the some_class_instance_variable class method in the User model itself?

After giving it some extra thought, I decided to simply store the some_class_instance_variable in the concern itself, and since the model User also called the some_class_macro_all_models_must_have , I decided to include the UserModule when it was invoked, effectively eliminating both the circular dependency and the load order issue.

The real code is much more complex than this contrived example, but the end result was something like this:

module TestConcern

  def self.some_class_instance_variable
    @some_class_instance_variable ||= Set.new
  end

  extend ActiveSupport::Concern

  class_methods do

    def some_class_macro_all_models_must_have

      User.include(UserModule) if self == User

      TestConcern.some_class_instance_variable << self

    end

  end

  included do
  end

  module UserModule

    def self.included(base)
      base.class_eval do

        # Class macro invocations, class method and instance method definitions

      end
    end

  end

end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM