简体   繁体   中英

Class Methods Only Included Once When Using Concerns in Rails 3 with Namespaced Models

I have a folder structure that looks like the following:

app/models/
    concerns/
        quxable.rb
    foo/
        bar.rb
        baz.rb

I'm in Rails 3 so I've autoloaded my concerns with:

config.autoload_paths += Dir[Rails.root.join('app', 'models', "concerns", '**/')]

And the files are as follows:

quxable.rb

module Quxable
    extend ActiveSupport::Concern        

    module ClassMethods
        def new_method
        end
    end
end

bar.rb

class Foo::Bar < ActiveRecord::Base
    include Quxable
end

baz.rb

class Foo::Baz < ActiveRecord::Base
    include Quxable
end

Now in the console if do this, I get the following outputs:

Foo::Bar.respond_to? :new_method #=> true
Foo::Baz.respond_to? :new_method #=> false
reload!
Foo::Baz.respond_to? :new_method #=> true
Foo::Bar.respond_to? :new_method #=> false

So it would seem to only be properly included on the model that is first accessed. And yet, If I run the following:

ActiveRecord::Base.descendants.select{ |c| c.included_modules.include?(Quxable) }.map(&:name)

I get ["Foo::Bar", "Foo::Baz"] .

Any idea what's going on here? I'm guessing something with autoloading/eagerloading, but I'm not sure why both models aren't getting the new class method.

PS - I've tried rewriting the module without ActiveSupport::Concern (just because I'm on an old Rails version and I'm taking shots in the dark) using:

def include(base)
    base.send :extend, ClassMethods
end

but I still have the same problem.

EDIT

I initially left this out (just trying to present the simplest problem), so I apologize to those trying to help earlier. But quxable.rb actually looks like this:

module Quxable
    extend ActiveSupport::Concern 

    LOOKUP = {
        Foo::Bar => "something",
        Foo::Baz => "something else"
    }

    module ClassMethods
        def new_method
        end
    end
end

So I'm guessing I created some kind of circular dependency defining a constant with the Class objects. Can anyone confirm? Weird that it just fails silently by not defining the class methods on the class that's accessed second though. I don't know why that is?

Based on your edit, this code is problematic:

LOOKUP = {
    Foo::Bar => "something",
    Foo::Baz => "something else"
}

It will instantiate Foo::Bar before the module is complete. Thus new_method will be omitted. Tradition in this case is to use strings in the lookup and .constantize them to turn them into classes when needed.

LOOKUP = {
    "Foo::Bar" => "something",
    "Foo::Baz" => "something else"
}

then

LOOKUP.keys.first.constantize.new_method

or

result = LOOKUP[Foo::Bar.name]

to use it.

I think you have a typo, and concerns include some magic that lets you transcend the limitations on mix-ins.

Also, if you're working in a directory under something that's already autoloaded, like 'models', just namespace everything to that directory name.

Try this:

module Concerns
  module Quxable

    extend ActiveSupport::Concern

    included do
      def self.new_method
      end
    end
  end
end


module Foo
  class Baz < ActiveRecord::Base
    include Concerns::Quxable
  end
end

As far as I remember you shouldn't need the extra autoload directive, as using the namespace in a directory under models will just work.


Edit after comments:

I've set up a Rails project with the following added files:

app/models/foo/doer.rb

app/models/foo/thinker.rb

app/models/concerns/thingable.rb

thingable.rb is:

module Concerns
  module Thingable
    extend ActiveSupport::Concern
    included do
      def self.thing
      end
    end
  end
end

doer.rb is:

module Foo
  class Doer < ActiveRecord::Base
    include Concerns::Thingable
  end
end

thinker.rb is:

module Foo
  class Thinker < ActiveRecord::Base
    include Concerns::Thingable
  end
end

In a console:

Loading development environment (Rails 3.2.22)

2.1.3 :001 > Foo::Doer.respond_to? :thing

=> true

2.1.3 :002 > Foo::Thinker.respond_to? :thing

=> true

2.1.3 :003 > reload!

Reloading...

=> true

2.1.3 :004 > Foo::Doer.respond_to? :thing

=> true

2.1.3 :005 > Foo::Thinker.respond_to? :thing

=> true

2.1.3 :006 >

I did not change the autoloading at all, I relied on Rails to find files based on namespacing. (Use a namespace for directories under known directories like 'models')

I would reset your autoloading to default, then use Rails conventions for file locations and namespacing. If that doesn't work, there may be other things your project is doing that I don't know about.

Let me know if you can provide any more details.

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