Rails STI and multi-level inheritance queries - ruby-on-rails

In my database I have a table people, and I'm using single table inheritance, with these classes:
class Person < ActiveRecord::Base
end
class Member < Person
end
class Business < Member
end
The queries it generates confuse me. What I want is for Member.all to return all Businesses as well as any other subtypes of Member. Which it does, but only if I've accessed the Business class recently. I assume it's because my classes aren't being cached in development mode (for obvious reasons), but it still seems like strange/buggy behaviour.
Is this a bug in rails? Or is it working as intended? In either case, can anyone think of a good fix for development purposes?

This is intentional behaviour—the official Rails guide on Autoloading and Reloading Constants explains it pretty well in the section on Autoloading and STI:
…
A way to ensure this works correctly regardless of the order of
execution is to load the leaves of the tree by hand at the bottom of
the file that defines the root class:
# app/models/polygon.rb
class Polygon < ApplicationRecord
end
require_dependency 'square'
Only the leaves that are at least grandchildren need to be loaded this
way. Direct subclasses do not need to be preloaded. If the hierarchy
is deeper, intermediate classes will be autoloaded recursively from
the bottom because their constant will appear in the class definitions
as superclass.
So in your case, this would mean putting an require_dependency "business" at the end of your Person class.
However, beware of circular dependencies which can possibly be avoided by using require instead of require_dependency (even though it may prohibit Rails from tracking and reloading your files when changes are made—after all, require_dependency is a Rails-internal method).

By default, Rails is not eager loading your classes in development. Try changing the following line in your config/environments/development.rb:
# Do not eager load code on boot.
config.eager_load = false
to:
# Do eager load code on boot!
config.eager_load = true

Related

Zeitwerk + Rails 6.1 with model and namespaced subclasses, "earlier autoload discarded"

After researching this on SO and a very similar issue in Rails' GitHub issues, I'm still unclear what's wrong. My namespaced model subclasses are not eager-loaded, but I believe they are declared correctly and in the right place.
They do seem to be autoloaded and are accessible, but each one does not show up in subclasses of the parent class until they are instantiated.
The parent model:
# /app/models/queued_email.rb
class QueuedEmail < ApplicationRecord
end
My namespaced subclass models (there are a dozen):
# /app/models/queued_email/comment_notification.rb
class QueuedEmail::CommentNotification < QueuedEmail
end
# or alternatively (this also doesn't eager load):
module QueuedEmail
class CommentNotification < QueuedEmail
end
end
The relevant message from Rails.autoloaders.log! (in config/application.rb)
Zeitwerk#rails.main: autoload set for QueuedEmail, to be autovivified from /vagrant/rails_app/app/models/queued_email
Zeitwerk#rails.main: earlier autoload for QueuedEmail discarded, it is actually an explicit namespace defined in /vagrant/rails_app/app/models/queued_email.rb
Zeitwerk#rails.main: autoload set for QueuedEmail, to be loaded from /vagrant/rails_app/app/models/queued_email.rb
If I open rails console and call subclasses, i get nothing:
> QueuedEmail
=> QueuedEmail (call 'QueuedEmail.connection' to establish a connection)
> QueuedEmail.subclasses
[]
But then... the subclass is accessible.
> QueuedEmail::CommentNotification
=> QueuedEmail::CommentNotification(id: integer...)
> QueuedEmail::CommentNotification.superclass
=> QueuedEmail(id: integer...)
> QueuedEmail.subclasses
=> [QueuedEmail::CommentNotification(id: integer...)]
I get nothing in subclasses until each one is instantiated in the code. Is my app/models folder incorrectly organized, or my subclasses incorrectly named?
Let me first explain the log messages.
Zeitwerk scans the project, and found a directory called queued_email before finding queued_email.rb. So, as a working hypothesis it assumed QueuedEmail was an implicit namespace with the information that it had. This hypothesis got later invalidated when it saw queued_email.rb, and said "wait, this is actually an explicit namespace". So it undid the implicit setup, and redefined it to load an explicit namespace.
Now, let's go for the subclasses.
When an application does not eager load, files are only loaded on demand. For example, if you load QueuedEmail, and app/models/queued_email has 24 files recursively, none of them are loaded until they are used.
When a class is subclassed, the collection returned by subclasses is populated. But you don't know a class is subclassed until the subclass is loaded. Therefore, in a lazy loading environment subclasses is empty at the start. If you load 1 subclass it will have that one, but not the rest, until they are all eventually loaded.
If you need the subclasses to be there for the application to function properly, starting with Zeitwerk 2.6.2 you can throw this to an initializer
# config/initializers/eager_load_queued_email.rb
Rails.application.config.to_preprare do
Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/queued_email")
end

Ruby on Rails extending a gem's class and changing its inheriting class

I have a web app built on Ruby on Rails.
In this app obviously use different Gems.
Iv come to a point where i want to extend some specific gem's classes, and add methods to it.
Now i have a use case where i want to extend a gem's class, but instead of adding methods, i want to change its inherting class
lets take the Impressionist Gem for example:
Iv created a new class in my app - app/models/impression.rb
class Impression < ActiveRecord::Base
establish_connection(ENV[App_LOGS_DB'])
end
i want to change the Inheritance to use a my custom class called LogsBase
class Impression < LogsBase
end
the LogsBase class is definend as follows:
class LogsBase < ActiveRecord::Base
establish_connection ENV['APP_LOGS_DB']
self.abstract_class = true
end
When in try to run the server, the following exception is raised:
/gems/impressionist-1.6.1/lib/impressionist/models/active_record/impression.rb:5:in `<main>': superclass mismatch for class Impression (TypeError)
which from my understanding basically means there is a conflict between the gem's impression class definition and my own extention of that class.
Can anyone please help in finding out a way that i can change the Impression class inherting class, while still perserving the class's behaviour and making my server run properly?
PS: the goal of all this is to write the impressions data into a different database (logs db) rather than the main app db. in order to do that i need to establish a connection to the logs db, but if ill do it within the Impression class directly , it will blow up my pool of DB connections as indicated in the following link:
https://www.thegreatcodeadventure.com/managing-multiple-databases-in-a-single-rails-application/
thats why i need the abstract LogsBase class.
Any help will be appreciated.
Disclaimer: do not do that!
The only way I can think of is a nasty hack, neither reliable, nor robust, that is not compatible with newer versions of your external gem. Since Ruby does not allow to redefine the classes ancestors, you might (but please don’t):
grab the content of original /gems/impressionist-1.6.1/lib/impressionist/models/active_record/impression.rb file.
copy-paste it into your /blah/foo/impression.
make your class be loading impressionist/models/active_record/impression explicitly.
in the very second line of the file unset original class.
Something like this:
require 'impressionist/models/active_record/impression'
Object.send :remove_const, 'Impression'
class Impression < LogsBase
# ORIGINAL CONTENT OF THIS FILE
end
There is no (sensible) way to redefine a base class in ruby. It's possible, but only via weird hacks.
I would suggest taking one of two routes here:
Fork the library, and make the base class configurable (with backwards compatibility).
Don't do this via inheritance. Instead, put establish_connection ENV['SPLACER_LOGS_DB'] into a module, and include it in the class.
I would be inclined to use option 2 for now, as it's a quick/simple workaround that should fit well with the rest of the application.

Make Rails autoloading/reloading follow dynamic includes

Context
I want to add some admin specific code to all models via concerns that are automatically included. I'm using the naming convention MyModel => MyModelAdmin and they're in the standard Rails directory app/models/concerns/my_model_admin.rb. Then I can glob over all of them and do MyModel.include(MyModelAdmin).
Issue
Dynamic includes work fine, but when changing the concern in development Rails doesn't reload it properly. In fact the concern seems to get removed.
Reproduction
app/models/my_model.rb
class MyModel
end
app/models/concerns/my_model_admin.rb
module MyModelAdmin
extend ActiveSupport::Concern
def say
"moo"
end
end
config/initializers/.rb
MyModel.include(MyModelAdmin)
So far so good, MyModel.new.say == "moo".
But now change say to "baa" and you get NoMethodError undefined method 'say'.
Notes
I tried a number of things that didn't help:
require_dependency
Model.class_eval "include ModelAdministration"
config.autoload_paths
Using another explicit concern in ApplicationModel with an included hook that includes the specific concern in each model.
ActiveSupport.on_load only triggered on Base not each model.
Does this mean Rails can only autoload using static scope? I guess Rails sees the concern change, knows the model has it included, reloads the model but the static model definition doesn't have the include so the concern goes missing and stops being tracked. Is there a way to force Rails to track dynamically included modules?

Is the "uninitialized constant" error related to the loading order of classes?

I am using Ruby v1.9.2 and Ruby on Rails v3.2.2. I have many model classes having constant statements. For instance:
# app/models/class_one.rb
class ClassOne < ActiveRecord::Base
CONSTANT_ONE = ClassTwo::CONSTANT_TWO
end
# app/models/class_two.rb
class ClassTwo < ActiveRecord::Base
CONSTANT_TWO = 1
end
When I restart the server, I get the following error:
Routing Error
uninitialized constant ClassTwo::CONSTANT_TWO
Try running rake routes for more information on available routes.
Is the error related to the loading order of files (and so of classes)? How should I solve the problem?
Note: Since Ruby on Rails, I heard that a "working" solution could be to state constants in initializer files (in the config/initializers/ directory). If so, how should that be made the proper way? What do you think about?
Constants in Rails are kind of a pain, as you are beginning to find out. The pain only increases as you really dig in. It is much easier and more maintainable to use an actual method on the class than to use a constant. For example, in testing, it is MUCH easier to modify a method than a constant when covering a variety of use cases. Also, when doing more complicated programming, you can begin to get into loading issues (like multiple loading errors, or unavailability, like you have) that just don't happen with methods. I've stopped using constants in my Rails apps altogether, and haven't missed them a bit. You may be interested in an article that Advi Grimm wrote to the same effect.
Edit:
If you really desire to use constants, in the way you described, check out Where's the best place to define a constant in a Ruby on Rails application? for more info.
Those 2 classes are defined in the same file? wow. Reorder the classes:
class ClassTwo < ActiveRecord::Base
CONSTANT_TWO = 1
end
class ClassOne < ActiveRecord::Base
CONSTANT_ONE = ClassTwo::CONSTANT_TWO
end
should fix it. CONSTANT_ONE = ClassTwo::CONSTANT_TWO is evaluated as soon as it's parsed.

Loading ActiveRecord models in the proper order outside of a rails app

How do I load/require my activerecord models in the proper order outside of a rails app. I have many STI models and I am getting an uninitialized constant exception.
$:.push File.expand_path("../../../app/models", __FILE__)
require "active_record"
Dir["#{File.expand_path('../../../app/models', __FILE__)}/*.rb"].each do |path|
require "#{File.basename(path, '.rb')}"
end
I have a lot of jobs that I need to run with resque and I would rather not have my rails app load everytime and be deployed to all of the worker machines
EDIT: One point to clarify as well. There are two projects a Rails project and a project that is a rails engine which contains my models. I dont load the rails engine itself with my resque jobs I just use the snippet above in a separate class to load active record on the models. This always worked until I added some STI models which because of the naming caused the children to attempt to be loaded before the parent. The rails engine project loads just fine in the rails project no issues there this is just because I am trying to use active record outside of a rails project.
A very simple solution if you don't want to autoload is to require the base class in the children classes. Explicitly requiring dependencies is a good thing. :)
app/models/profile.rb
class Profile < ActiveRecord::Base
end
app/models/student.rb
require 'models/profile'
class Student < Profile
end
app/models/teacher.rb
require 'models/profile'
class Teacher < Profile
end
Models will be autoloaded on their first mention. So just name them somewhere in a proper order (say, in config/initializers/load_order.rb):
Product
LineItem
Cart
and check if it helps.
I fixed my issue. There may be a better way but this does it for me.
basedir = File.expand_path('../../../app/models', __FILE__)
Dir["#{basedir}/*.rb"].each do |path|
name = "#{File.basename(path, '.rb')}"
autoload name.classify.to_sym, "#{basedir}/#{name}"
end

Resources