I want to combine the two following things:
My STI structure:
Assume a Rails model User. There are only two types of users, namely Admin and Customer. The STI column is type, the table is called users but in practice, we will never have and User objects around, just Admins and Customers.
User(used for storing all data and implementing common functionality, but never instanciated)AdminCustomer < User
Dynamically extend models:
In some cases, it is handy to dynamically extend a model class, to temporarely have an object with more capabilities to carry out a specific action. Assume my create controller action checks for an additional field "Send welcome mail" (which is provided by the new form). The attribute is virtual, thus we can use it in the form as if it was a normal column. This can be realized with:
extended_user_class = Class.new(User)
extended_user_class.send(:include, MyAwesomeMixins)
extended_user_class.class_eval do
my_virtual_attribute :send_welcome_mail, default: true
end
model = extended_user_class.new
# send model to the view and it "just works"
Combining both
Unfortunately combining both of these techniques does not work in the way described above, because Rails STI appears to be broken by anonymous classes:
[1] pry(main)> User.where(id: 1).to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`id` = 1"
[2] pry(main)> Class.new(User).where(id: 1).to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`type` IS NULL AND `users`.`id` = 1"
EDIT: To reproduce the above, must be in production env.
The second statement is wrong because it filters by type. The first statement is the intended behavior.
So, apearently anonymous classes are bad for STI. Redefining the constant User as described in https://dev.to/factorial/a-trick-with-ruby-anonymous-classes-11pp is likely to break the application - I want the extended class to only be used when called explicitely (extended_user_class) - User has to be unaffected.
Can this be achieved in Ruby 3 / Rails 6?
Two potential solutions:
Singleton class
Experimenting showed that singleton classes (also known as Eigenclasses) would likely provide a solution, however there could be a performance loss of down to 25%.
So the part dynamically extending the model would become:
Patch the anonymous class to pretend that it is the superclass
In this solution, the anonymous class is extended with a mixin that delegates the following three class methods to
superclass:finder_needs_type_condition?descendantsnameFor instance:
This way, the anonymous class becomes transparent and behaves like the class it inherits from. This solution is a rather ugly hack but it appears to work in practice. It is uglier but faster than the singleton class solution.
There is one caveat however:
As can be seen, the anonymous class
extended_admin1extendsUser, notAdmin.