ALL ARTICLES
SHARE

Rails & Devise: X is not a subclass of User

Bryan Villafañe
Development
4 min read
not a subclass of user

Our team recently ran into a maddening issue with Ruby on Rails, the Devise gem, and Single Table Inheritance (STI). Eventually, the issue got so bad that every single backend change triggered the exception ActiveRecord::SubclassNotFound with the message Invalid single-table inheritance type: ProjectManager is not a subclass of User. The problem was, ProjectManager was a subclass of User. The two classes shared a database table called “users” and ProjectManager inherited from User within the codebase. As it turns out, the fact that ProjectManager used STI and inherited it from User was the core of the issue.

Single Table Inheritance and Rails Autoloading

After some investigation, we figured out that autoloading in Ruby on Rails doesn’t play well with STI. The fact that single table inheritance doesn’t work well with auto-loading is actually documented in the Rails documentation. However, the solution that the Rails documentation provides for dealing with autoloading issues related to STI didn’t work for us, so we had to come up with our own.

Here is a partial stack trace from our Rails app showing the inheritance issue:

ActiveRecord::SubclassNotFound in UsersController#update

Invalid single-table inheritance type: ProjectManager is not a subclass of User
Extracted source (around line #241):
#239 end
#240 unless subclass == self || descendants.include?(subclass)
*241 raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}"
#242 end
#243 subclass
#244 endExtracted source (around line #215):
#213 def discriminate_class_for_record(record)
#214 if using_single_table_inheritance?(record)
*215 find_sti_class(record[inheritance_column])
#216 else
#217 super
#218 end

 

Extracted source (around line #257):
#255 # how this "single-table" inheritance mapping is implemented.
#256 def instantiate(attributes, column_types = {}, &block)
*257 klass = discriminate_class_for_record(attributes)
#258 instantiate_instance_of(klass, attributes, column_types, &block)
#259 end
#260

Rails.root: rails_project/project
Application Trace
app/controllers/users_controller.rb:6:in `update'
Framework Trace
activerecord (6.0.3.6) lib/active_record/inheritance.rb:241:in `find_sti_class'
activerecord (6.0.3.6) lib/active_record/inheritance.rb:215:in `discriminate_class_for_record'
activerecord (6.0.3.6) lib/active_record/persistence.rb:257:in `instantiate'
activerecord (6.0.3.6) lib/active_record/querying.rb:58:in `block (2 levels) in find_by_sql'
activerecord (6.0.3.6) lib/active_record/result.rb:62:in `block in each'
activerecord (6.0.3.6) lib/active_record/result.rb:62:in `each'
activerecord (6.0.3.6) lib/active_record/result.rb:62:in `each'
activerecord (6.0.3.6) lib/active_record/querying.rb:58:in `map'
activerecord (6.0.3.6) lib/active_record/querying.rb:58:in `block in find_by_sql'
activesupport (6.0.3.6) lib/active_support/notifications/instrumenter.rb:24:in `instrument'
activerecord (6.0.3.6) lib/active_record/querying.rb:56:in `find_by_sql'
activerecord (6.0.3.6) lib/active_record/statement_cache.rb:134:in `execute'
activerecord (6.0.3.6) lib/active_record/core.rb:204:in `find_by'
devise-jwt (0.8.0) lib/devise/jwt/models/jwt_authenticatable.rb:20:in `find_for_jwt_authentication'

ActiveRecord::SubclassNotFound in UsersController#update

Invalid single-table inheritance type: ProjectManager is not a subclass of User
Extracted source (around line #241):
#239 end
#240 unless subclass == self || descendants.include?(subclass)
*241 raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}"
#242 end
#243 subclass
#244 endExtracted source (around line #215):
#213 def discriminate_class_for_record(record)
#214 if using_single_table_inheritance?(record)
*215 find_sti_class(record[inheritance_column])
#216 else
#217 super
#218 end

 

Extracted source (around line #257):
#255 # how this "single-table" inheritance mapping is implemented.
#256 def instantiate(attributes, column_types = {}, &block)
*257 klass = discriminate_class_for_record(attributes)
#258 instantiate_instance_of(klass, attributes, column_types, &block)
#259 end
#260

Rails.root: rails_project/project
Application Trace
app/controllers/users_controller.rb:6:in `update'
Framework Trace
activerecord (6.0.3.6) lib/active_record/inheritance.rb:241:in `find_sti_class'
activerecord (6.0.3.6) lib/active_record/inheritance.rb:215:in `discriminate_class_for_record'
activerecord (6.0.3.6) lib/active_record/persistence.rb:257:in `instantiate'
activerecord (6.0.3.6) lib/active_record/querying.rb:58:in `block (2 levels) in find_by_sql'
activerecord (6.0.3.6) lib/active_record/result.rb:62:in `block in each'
activerecord (6.0.3.6) lib/active_record/result.rb:62:in `each'
activerecord (6.0.3.6) lib/active_record/result.rb:62:in `each'
activerecord (6.0.3.6) lib/active_record/querying.rb:58:in `map'
activerecord (6.0.3.6) lib/active_record/querying.rb:58:in `block in find_by_sql'
activesupport (6.0.3.6) lib/active_support/notifications/instrumenter.rb:24:in `instrument'
activerecord (6.0.3.6) lib/active_record/querying.rb:56:in `find_by_sql'
activerecord (6.0.3.6) lib/active_record/statement_cache.rb:134:in `execute'
activerecord (6.0.3.6) lib/active_record/core.rb:204:in `find_by'
devise-jwt (0.8.0) lib/devise/jwt/models/jwt_authenticatable.rb:20:in `find_for_jwt_authentication'

Why this Single Table Inheritance Issue Occurs

This bug took us quite a bit to figure out. We actually ended up with breakpoints sprinkled throughout both ActiveRecord and Devise. Eventually, we realized that we had hit an area in ActiveRecord.find_by where klass !== ProjectManager was true, even though klass was indeed ProjectManager.

What led us to realize that this was actually an autoloading issue was the fact that klass.object_id was not equal to ProjectManager.object_id. ActiveRecord was holding into a stale version of the ProjectManager class, and the equality comparison for the two classes was not passing because the object ids were different. When ProjectManager had been reloaded in our application, it had not been reloaded in the ActiveRecord gem. Thus, line #240 (specifically, descendants.include?(subclass)) in the code above would return false because the traversal of descendants would produce a different ProjectManager object_id than our codebase.

Fixing the STI Autoloading Issue

Hate to disappoint you if you read this far, but indeed we had to use Monkey Patching to solve this one. Unfortunately, we ultimately had to monkey patch the User class (or any parent class that uses STI and devise) to override the find_sti_class method added by Devise. We were, however, able to limit this monkey patch to development since classes don’t typically reload in production. I don’t love this solution but it enabled our Rails development environment to function similarly to our production environment (this issue didn’t occur in production).

1
2
3
4
5
6
7
8
class User < ApplicationRecord
  unless Rails.application.config.eager_load
    def self.find_sti_class(type_name)
      return User if type_name.to_s == ‘User’
      return ProjectManager if type_name.to_s == ‘ProjectManager’
    end
  end
end
1
2
3
4
5
6
7
8
class User < ApplicationRecord
  unless Rails.application.config.eager_load
    def self.find_sti_class(type_name)
      return User if type_name.to_s == ‘User’
      return ProjectManager if type_name.to_s == ‘ProjectManager’
    end
  end
end

This solution allowed the User and ProjectManager within our application code to match those within Devise, since the method was called from within our application as opposed to within Devise.

Conclusion

I find it very unfortunate that Rails offers a feature that it doesn’t fully support and actively documents issues with. I find Ruby on Rails a great technology, but this seems like poor development practice in my opinion. Hopefully, in a future version, either Single Table Inheritance support is removed, or autoloading actually works alongside it. I was surprised to read the documentation and read that it was essentially known that STI and autoloading don’t play together. The thing is that almost all Rails applications use autoloading in development environments, and this bug is not easy to reason about on its surface level.

Hopefully, this solution helps others. If you are facing an equality issue with a class that utilizes STI, I recommend you start looking at object ids.

Expert Ruby on Rails Development Services

Flatirons helps businesses create full-stack Ruby on Rails applications.

Learn more

Expert Ruby on Rails Development Services

Flatirons helps businesses create full-stack Ruby on Rails applications.

Learn more
Bryan Villafañe
More ideas.
grpc vs rest
Development

gRPC vs. REST: Navigating API Communication Standards

Flatirons

Jul 26, 2024
yarn vs npm
Development

Yarn vs. npm: Choosing the Best Package Manager

Flatirons

Jul 22, 2024
process analysis
Development

Mastering Process Analysis in Business

Flatirons

Jul 18, 2024
product development life cycle
Development

Navigating the Product Development Life Cycle

Flatirons

Jul 11, 2024
Kotlin vs Java
Development

Kotlin vs. Java: Choosing the Right Language for Your Project

Flatirons

Jul 08, 2024
OpenShift vs Kubernetes: 10 Differences
Business

OpenShift vs Kubernetes: 10 Differences

Flatirons

Jul 06, 2024
grpc vs rest
Development

gRPC vs. REST: Navigating API Communication Standards

Flatirons

Jul 26, 2024
yarn vs npm
Development

Yarn vs. npm: Choosing the Best Package Manager

Flatirons

Jul 22, 2024
process analysis
Development

Mastering Process Analysis in Business

Flatirons

Jul 18, 2024
product development life cycle
Development

Navigating the Product Development Life Cycle

Flatirons

Jul 11, 2024
Kotlin vs Java
Development

Kotlin vs. Java: Choosing the Right Language for Your Project

Flatirons

Jul 08, 2024
OpenShift vs Kubernetes: 10 Differences
Business

OpenShift vs Kubernetes: 10 Differences

Flatirons

Jul 06, 2024
grpc vs rest
Development

gRPC vs. REST: Navigating API Communication Standards

Flatirons

Jul 26, 2024
yarn vs npm
Development

Yarn vs. npm: Choosing the Best Package Manager

Flatirons

Jul 22, 2024
process analysis
Development

Mastering Process Analysis in Business

Flatirons

Jul 18, 2024
product development life cycle
Development

Navigating the Product Development Life Cycle

Flatirons

Jul 11, 2024
Kotlin vs Java
Development

Kotlin vs. Java: Choosing the Right Language for Your Project

Flatirons

Jul 08, 2024
OpenShift vs Kubernetes: 10 Differences
Business

OpenShift vs Kubernetes: 10 Differences

Flatirons

Jul 06, 2024
grpc vs rest
Development

gRPC vs. REST: Navigating API Communication Standards

Flatirons

Jul 26, 2024
yarn vs npm
Development

Yarn vs. npm: Choosing the Best Package Manager

Flatirons

Jul 22, 2024
process analysis
Development

Mastering Process Analysis in Business

Flatirons

Jul 18, 2024
product development life cycle
Development

Navigating the Product Development Life Cycle

Flatirons

Jul 11, 2024
Kotlin vs Java
Development

Kotlin vs. Java: Choosing the Right Language for Your Project

Flatirons

Jul 08, 2024
OpenShift vs Kubernetes: 10 Differences
Business

OpenShift vs Kubernetes: 10 Differences

Flatirons

Jul 06, 2024