From 497392d16af01fd5fc90819b5f6af0ed1f18fe86 Mon Sep 17 00:00:00 2001 From: Kirill Usanov <48535087+Exterm1nate@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:17:32 +0300 Subject: [PATCH] Fix reloading with custom classes (#22) --- CHANGELOG.md | 2 ++ README.md | 24 ++++++++++++++++++- app/models/active_fields/field.rb | 5 ++++ lib/active_fields/engine.rb | 3 ++- .../{ => active_fields}/custom_field.rb | 2 +- .../{ => active_fields}/custom_value.rb | 2 +- .../{ => active_fields}/ip_array_field.rb | 0 .../models/{ => active_fields}/ip_field.rb | 0 .../models/{ => core}/application_record.rb | 0 spec/dummy/config/application.rb | 10 ++++++++ 10 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 app/models/active_fields/field.rb rename spec/dummy/app/models/{ => active_fields}/custom_field.rb (93%) rename spec/dummy/app/models/{ => active_fields}/custom_value.rb (95%) rename spec/dummy/app/models/{ => active_fields}/ip_array_field.rb (100%) rename spec/dummy/app/models/{ => active_fields}/ip_field.rb (100%) rename spec/dummy/app/models/{ => core}/application_record.rb (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e2cfd..b44c444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ - Drop support for _Ruby_ < 3.1 (EOL) - Added search functionality - Added registry to store relationships between _Customizable_ types and _Active Field_ types +- Added notes about the necessity of disabling reloading for custom model classes and custom _Active Field_ type models +to prevent _STI_ (_Single Table Inheritance_) issues **Breaking changes**: - Maximum datetime precision reduced to 6 for all _Ruby_/_Rails_ versions. diff --git a/README.md b/README.md index e558ea7..5dd0a1a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Gem downloads count](https://img.shields.io/gem/dt/active_fields)](https://rubygems.org/gems/active_fields) [![Github Actions CI](https://github.com/lassoid/active_fields/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/lassoid/active_fields/actions/workflows/main.yml) -**ActiveFields** is a _Rails_ plugin that implements the _Entity-Attribute-Value (EAV)_ pattern, +**ActiveFields** is a _Rails_ plugin that implements the _Entity-Attribute-Value_ (_EAV_) pattern, enabling the addition of custom fields to any model at runtime without requiring changes to the database schema. ## Key Concepts @@ -596,6 +596,25 @@ class CustomValue < ApplicationRecord end ``` +**Note:** To avoid _STI_ (_Single Table Inheritance_) issues in environments with code reloading (`config.enable_reloading = true`), +you should ensure that your custom model classes, along with all their superclasses and mix-ins, are non-reloadable. +Follow these steps: +- Move your custom model classes to a separate folder, such as `app/models/active_fields`. +- If your custom model classes subclass `ApplicationRecord` (or other reloadable class) or mix-in reloadable modules, +move those superclasses and modules to another folder, such as `app/models/core`. +- After organizing your files, add the following code to your `config/application.rb`: + ```ruby + # Disable custom models reloading to avoid STI issues. + custom_models_dir = "#{root}/app/models/active_fields" + models_core_dir = "#{root}/app/models/core" + Rails.autoloaders.main.ignore(custom_models_dir, models_core_dir) + Rails.autoloaders.once.collapse(custom_models_dir, models_core_dir) + config.autoload_once_paths += [custom_models_dir, models_core_dir] + config.eager_load_paths += [custom_models_dir, models_core_dir] + ``` + This configuration disables namespaces for these folders + and adds them to `autoload_once_paths`, ensuring they are not reloaded. + ### Adding Custom Field Types To add a custom _Active Field_ type, create a subclass of the `ActiveFields.config.field_base_class`, @@ -682,6 +701,9 @@ class IpArrayField < ActiveFields.config.field_base_class end ``` +**Note:** Similar to custom model classes, you should disable code reloading for custom _Active Field_ type models. +Place them in the `app/models/active_fields` folder too. + For each custom _Active Field_ type, you must define a **validator**, a **caster** and optionally a **finder**: #### Validator diff --git a/app/models/active_fields/field.rb b/app/models/active_fields/field.rb new file mode 100644 index 0000000..36a4f2c --- /dev/null +++ b/app/models/active_fields/field.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module ActiveFields + module Field; end +end diff --git a/lib/active_fields/engine.rb b/lib/active_fields/engine.rb index 638184b..8fd2e70 100644 --- a/lib/active_fields/engine.rb +++ b/lib/active_fields/engine.rb @@ -8,7 +8,8 @@ class Engine < ::Rails::Engine # Disable models reloading to avoid STI issues. # Reloading can prevent subclasses from recognizing the base class. - config.autoload_once_paths = %W[#{root}/app/models #{root}/app/models/concerns] + config.autoload_once_paths << "#{root}/app/models" + config.autoload_once_paths << "#{root}/app/models/concerns" initializer "active_fields.active_record" do ActiveSupport.on_load(:active_record) do diff --git a/spec/dummy/app/models/custom_field.rb b/spec/dummy/app/models/active_fields/custom_field.rb similarity index 93% rename from spec/dummy/app/models/custom_field.rb rename to spec/dummy/app/models/active_fields/custom_field.rb index d935fae..78a2888 100644 --- a/spec/dummy/app/models/custom_field.rb +++ b/spec/dummy/app/models/active_fields/custom_field.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# If the field base class is set to default, we disallow loading the custom class. +# If the field base class is set to default, we disallow loading the custom model class. # This is an inversion of the same conditional statement from ActiveFields::Field::Base if ActiveFields.config.field_base_class_changed? class CustomField < ApplicationRecord diff --git a/spec/dummy/app/models/custom_value.rb b/spec/dummy/app/models/active_fields/custom_value.rb similarity index 95% rename from spec/dummy/app/models/custom_value.rb rename to spec/dummy/app/models/active_fields/custom_value.rb index ee45a23..151a7a8 100644 --- a/spec/dummy/app/models/custom_value.rb +++ b/spec/dummy/app/models/active_fields/custom_value.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# If the value class is set to default, we disallow loading the custom class. +# If the value class is set to default, we disallow loading the custom model class. # This is an inversion of the same conditional statement from ActiveFields::Value if ActiveFields.config.value_class_changed? class CustomValue < ApplicationRecord diff --git a/spec/dummy/app/models/ip_array_field.rb b/spec/dummy/app/models/active_fields/ip_array_field.rb similarity index 100% rename from spec/dummy/app/models/ip_array_field.rb rename to spec/dummy/app/models/active_fields/ip_array_field.rb diff --git a/spec/dummy/app/models/ip_field.rb b/spec/dummy/app/models/active_fields/ip_field.rb similarity index 100% rename from spec/dummy/app/models/ip_field.rb rename to spec/dummy/app/models/active_fields/ip_field.rb diff --git a/spec/dummy/app/models/application_record.rb b/spec/dummy/app/models/core/application_record.rb similarity index 100% rename from spec/dummy/app/models/application_record.rb rename to spec/dummy/app/models/core/application_record.rb diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index 2ea1b05..6c9ad17 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -32,6 +32,16 @@ class Application < Rails::Application # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w(assets tasks)) + # Disable custom models reloading to avoid STI issues. + # Since custom models are subclasses of ApplicationRecord, it should also not be reloadable. + # Move it to `app/models/core`. + custom_models_dir = "#{root}/app/models/active_fields" + models_core_dir = "#{root}/app/models/core" + Rails.autoloaders.main.ignore(custom_models_dir, models_core_dir) + Rails.autoloaders.once.collapse(custom_models_dir, models_core_dir) + config.autoload_once_paths += [custom_models_dir, models_core_dir] + config.eager_load_paths += [custom_models_dir, models_core_dir] + # Force UTC time zone config.time_zone = "UTC" config.active_record.default_timezone = :utc