Skip to content

Commit 01bc3a4

Browse files
committed
Document 4 ways to preload STIs
1 parent 76ca38e commit 01bc3a4

File tree

1 file changed

+64
-50
lines changed

1 file changed

+64
-50
lines changed

guides/source/autoloading_and_reloading_constants.md

Lines changed: 64 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -327,76 +327,90 @@ During eager loading, Rails invokes `Zeitwerk::Loader.eager_load_all`. That ensu
327327
Single Table Inheritance
328328
------------------------
329329

330-
Single Table Inheritance is a feature that doesn't play well with lazy loading. The reason is that its API generally needs to be able to enumerate the STI hierarchy to work correctly, whereas lazy loading defers loading classes until they are referenced. You can't enumerate what you haven't referenced yet.
330+
Single Table Inheritance doesn't play well with lazy loading: Active Record has to be aware of STI hierarchies to work correctly, but when lazy loading, classes are precisely loaded only on demand!
331331

332-
In a sense, applications need to eager load STI hierarchies regardless of the loading mode.
332+
To address this fundamental mismatch we need to preload STIs. There are a few options to accomplish this, with different trade-offs. Let's see them.
333333

334-
Of course, if the application eager loads on boot, that is already accomplished. When it does not, it is in practice enough to instantiate the existing types in the database, which in development or test modes is usually fine. One way to do that is to include an STI preloading module in your `lib` directory:
334+
### Option 1: Enable Eager Loading
335+
336+
The easiest way to preload STIs is to enable eager loading by setting:
335337

336338
```ruby
337-
module StiPreload
338-
unless Rails.application.config.eager_load
339-
extend ActiveSupport::Concern
340-
341-
included do
342-
cattr_accessor :preloaded, instance_accessor: false
343-
end
344-
345-
class_methods do
346-
def descendants
347-
preload_sti unless preloaded
348-
super
349-
end
350-
351-
# Constantizes all types present in the database. There might be more on
352-
# disk, but that does not matter in practice as far as the STI API is
353-
# concerned.
354-
#
355-
# Assumes store_full_sti_class is true, the default.
356-
def preload_sti
357-
types_in_db = \
358-
base_class.
359-
unscoped.
360-
select(inheritance_column).
361-
distinct.
362-
pluck(inheritance_column).
363-
compact
364-
365-
types_in_db.each do |type|
366-
logger.debug("Preloading STI type #{type}")
367-
type.constantize
368-
end
369-
370-
self.preloaded = true
371-
end
372-
end
373-
end
374-
end
339+
config.eager_load = true
375340
```
376341

377-
and then include it in the STI root classes of your project:
342+
in `config/environments/development.rb` and `config/environments/test.rb`.
343+
344+
This is simple, but may be costly because it eager loads the entire application on boot and on every reload. The trade-off may be worthwhile for small applications, though.
345+
346+
### Option 2: Preload a Collapsed Directory
347+
348+
Store the files that define the hierarchy in a dedicated directory, which makes sense also conceptually. The directory is not meant to represent a namespace, its sole purpose is to group the STI:
349+
350+
```
351+
app/models/shapes/shape.rb
352+
app/models/shapes/circle.rb
353+
app/models/shapes/square.rb
354+
app/models/shapes/triangle.rb
355+
```
356+
357+
In this example, we still want `app/models/shapes/circle.rb` to define `Circle`, not `Shapes::Circle`. This may be your personal preference to keep things simple, and also avoids refactors in existing code bases. The [collapsing](https://github.com/fxn/zeitwerk#collapsing-directories) feature of Zeitwerk allows us to do that:
378358

379359
```ruby
380-
# app/models/shape.rb
381-
require "sti_preload"
360+
# config/initializers/preload_stis.rb
382361

383-
class Shape < ApplicationRecord
384-
include StiPreload # Only in the root class.
362+
unless Rails.application.config.eager_load
363+
shapes = "#{Rails.root}/app/models/shapes"
364+
Rails.autoloaders.main.collapse(shapes) # Not a namespace.
365+
Rails.application.config.to_prepare do
366+
Rails.autoloaders.main.eager_load_dir(shapes)
367+
end
385368
end
386369
```
387370

371+
In this option, we eager load these few files on boot and reload even if the STI is not used. However, unless your application has a lot of STIs, this won't have any measurable impact.
372+
373+
INFO: The method `Zeitwerk::Loader#eager_load_dir` was added in Zeitwerk 2.6.2. For older versions, you can still list the `app/models/shapes` directory and invoke `require_dependency` on its contents.
374+
375+
WARNING: If models are added, modified, or deleted from the STI, reloading works as expected. However, if a new separate STI hierarchy is added to the application, you'll need to edit the initializer and restart the server.
376+
377+
### Option 3: Preload a Regular Directory
378+
379+
Similar to the previous one, but the directory is meant to be a namespace. That is, `app/models/shapes/circle.rb` is expected to define `Shapes::Circle`.
380+
381+
For this one, the initializer is the same except no collapsing is configured:
382+
388383
```ruby
389-
# app/models/polygon.rb
390-
class Polygon < Shape
384+
# config/initializers/preload_stis.rb
385+
386+
unless Rails.application.config.eager_load
387+
Rails.application.config.to_prepare do
388+
Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/shapes")
389+
end
391390
end
392391
```
393392

393+
Same trade-offs.
394+
395+
### Option 4: Preload Types from the Database
396+
397+
In this option we do not need to organize the files in any way, but we hit the database:
398+
394399
```ruby
395-
# app/models/triangle.rb
396-
class Triangle < Polygon
400+
# config/initializers/preload_stis.rb
401+
402+
unless Rails.application.config.eager_load
403+
Rails.application.config.to_prepare do
404+
types = Shape.unscoped.select(:type).distinct.pluck(:type)
405+
types.compact.each(&:constantize)
406+
end
397407
end
398408
```
399409

410+
WARNING: The STI will work correctly even if the table does not have all the types, but methods like `subclasses` or `descendants` won't return the missing types.
411+
412+
WARNING: If models are added, modified, or deleted from the STI, reloading works as expected. However, if a new separate STI hierarchy is added to the application, you'll need to edit the initializer and restart the server.
413+
400414
Customizing Inflections
401415
-----------------------
402416

0 commit comments

Comments
 (0)