Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

原因

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 があれば基本的には大丈夫なはず。

参考

HAZI
主要言語は JavaScript, Ruby, Perl など。最近は Ruby 中心。 業務システム開発を主に行っている。その他、デザインなども。
http://hazi.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away