原因
superclass mismatch for class Product (TypeError)
Concern を追加したら、Production 環境でエラーが発生
問題のコードが下記のようになっており、
class Product < ApplicationRecord
include Register
...
end
class Product
module Register
extend ActiveSupport::Concern
...
end
end
Concern の方が先に require/load されてしまうと、下記のようなコードを実行したのと同じ状況になり、TypeError
が発生してしまう。
**注意:**そもそもの問題として、model に依存している concern だからという理由で、class 名の名前空間に concern を置いているのが原因で、それ自体を見直した方がいい
class Product
end
class Product < ApplicationRecord
end
解決策
もっとも素直なやり方としては、Concern にも super class ApplicationRecord
を書いてしまう。
class Product < ApplicationRecord
module Register
extend ActiveSupport::Concern
...
end
end
でも、これは2度定義している感じが気持ちが悪い。
そもそも2度定義しているのが問題なので、下記のように修正して見た
module Product::Register
extend ActiveSupport::Concern
...
end
$ RAILS_ENV=production rails c
/project/vendor/bundle/ruby/2.5.0/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:500:in `load_missing_constant': Circular dependency detected while autoloading constant Product::Register (RuntimeError)
from /project/vendor/bundle/ruby/2.5.0/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/active_support.rb:43:in `load_missing_constant'
from /project/vendor/bundle/ruby/2.5.0/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:193:in `const_missing'
from /project/app/models/product.rb:48:in `<class:Product>'
from /project/app/models/product.rb:47:in `<main>'
...
今度は Concern ロードしようとしたら、Model ロードする必要が出たのでロードしたら、Model で include してる Concern ロードする必要が出たので…というループに入ってしまった様子
Concern からではなく、Model からロードした場合には class 定義を終えた後で Concern をロードしようとするので、この矛盾は発生しなくなるので、事前に Model をロードするようにして見た。
require 'product'
これで一応解決。
でも、なんとなく require そのまま書くのが違う気がしたので(今後同じことが起きる可能性あるし)、
require
してしまうと、開発環境での reload!
などが効かなくなってしまい、
spring 実行環境下や、rails server
実行中に concerns 以下のファイルの編集を行ったりすると、A copy of XXX has been removed from the module tree but is still active!
などと怒られてしまうことがあります。
なので、app/models/concerns
直下のディレクトリ名を元にクラス名を eval
して、model class を autoload してしまうことにしました。
# Concern を利用するモデルを事前に autoload します。
# TODO: バグで動かない為、後半のコードを利用(ruby 2.6 以降なら動くはず)
# @see https://bugs.ruby-lang.org/issues/14899
# Rails.root.join('app/models/concerns').glob('**/*/').each do |p|
# dir_name = %r{/app/models/concerns/(.+?)/?$}.match(p.to_s)&.captures&.first
# Object.instance_eval(dir_name.camelize) if dir_name
# end
Dir.glob("#{Rails.root}/app/models/concerns/**/*/").each do |p|
dir_name = %r{#{Rails.root}/app/models/concerns/(.+?)/?$}.match(p.to_s)&.captures&.first
Object.instance_eval(dir_name.camelize) if dir_name
end
もし、モデル名以外のディレクトリを置いたとしてもディレクトリ名と同じ class/module があれば基本的には大丈夫なはず。