LoginSignup
1
1

More than 5 years have passed since last update.

Rails に Concerns を追加して発生した `TypeError: superclass mismatch for XXX` の解決

Last updated at Posted at 2018-07-04

原因

superclass mismatch for class Product (TypeError)

Concern を追加したら、Production 環境でエラーが発生

問題のコードが下記のようになっており、

app/models/product.rb
class Product < ApplicationRecord
  include Register

  ...
end
app/models/concerns/register.rb
class Product
  module Register
    extend ActiveSupport::Concern

    ...
  end
end

Concern の方が先に require/load されてしまうと、下記のようなコードを実行したのと同じ状況になり、TypeError が発生してしまう。

注意:そもそもの問題として、model に依存している concern だからという理由で、class 名の名前空間に concern を置いているのが原因で、それ自体を見直した方がいい

class Product
end
class Product < ApplicationRecord
end

参考: クラス/メソッドの定義 (Ruby 2.5.0)

解決策

もっとも素直なやり方としては、Concern にも super class ApplicationRecord を書いてしまう。

app/models/concerns/register.rb
class Product < ApplicationRecord
  module Register
    extend ActiveSupport::Concern

    ...
  end
end

でも、これは2度定義している感じが気持ちが悪い。
そもそも2度定義しているのが問題なので、下記のように修正して見た

app/models/concerns/register.rb
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 をロードするようにして見た。

config/initializers/requirements.rb
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 してしまうことにしました。

config/initializers/requirements.rb
# 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 があれば基本的には大丈夫なはず。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1