ActiveRecordInclude
Have you ever wanted to make a module get included into all of your models so that you don't have to remember to include in each individual model?
Sure, if all you need is for some methods to be available in all your models, then you can just
include the module in your ApplicationRecord and all subclasses of ApplicationRecord will have
access to those methods by inheritance.
But if it's a ActiveSupport::Concern with a included blocks or similar that needs to do
something specific for each individual model subclass, then that approach doesn't work.
You can do something like this to automatically include MyConcern into all subclasses of
ApplicationRecord:
module MyExtensions
module ClassMethods
private
# These extensions aren't on (or can't be on) Base itself but on every model (every subclass of
# Base)
def inherited(subclass)
super
subclass.class_eval do
include MyConcern
end
end
end
end
class ApplicationRecord < ActiveRecord::Base
include MyExtensions
endOr you could use this gem and just write:
class ApplicationRecord < ActiveRecord::Base
include_when_inherited MyConcern
endHowever, even that doesn't always work. If we try to connect to a table from MyConcern that way,
we'll end up getting an error like this:
PG::UndefinedTable: ERROR: relation "application_records" does not exist
Model extensions/concerns that require an database connection (such as those that get a list of columns for
the model) can't be automatically included from inherited() because that's too early (inherited
gets called the moment it hits the class Model < ApplicationRecord line), before the body of the class
definition has been evaluated, and thus before the model (or its ancestors) has had a chance to
configure itself with self.abstract_class= or self.table_name= yet, for example.
Do this to have it include the given module in all (non-abstract) model subclasses as soon as that model has connected to its database:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include_when_connected NormalizeTextColumns
endBy default, it includes the module into all subclasses. If you don't want it to include the module into subclasses, pass recursive: false.
class SomeModel < ActiveRecord::Base
include_when_connected MyConcern, recursive: false
endLimitations
ActiveSupport::Concern modules are only included if the module is not already an ancestor
This means that the order you include concerns matters.
If you have AA < A < ApplicationRecord and you include the concern into A and then AA, when
you include it into AA the included block will not be run for AA, because it has already been
run for an ancestor of AA, namely A.
If on the other hand you somehow managed to include the concern into AA before it got included
into A, you can get it include the included block of the concern for both AA and A. But
this is easier said then done, because A needs to be defined first before you can
subclass it, and usually they are in different files so it would get included into A as soon as
the file for A was loaded. You would have to be very, very intentional about it, and reopen A,
like this:
class A < ApplicationRecord
end
class AA < A
include MyConcern
end
class A
include MyConcern
end
This is a limitation of ActiveSupport::Concern, not of this gem. (include_when_inherited calls
include to include the concern, but it has no effect.)
Often this limitation isn't a problem, because all of your models are direct descendents of
ApplicationRecord and you don't need to also include the concern into ApplicationRecord, for
example.
But if you really need a module's features to be appended (its included code) into both a model
and its superclass, one way to work around this limitation is to define your own included
callback, like we did in
spec/support/models/concerns/creature_self_identification.rb:
def self.included(base)
base.class_eval do
puts "#{self}: included CreatureSelfIdentification"
extend ClassMethods
define_identity_methods
end
endThen you can call include_recursively from the base class of the class hierarchy you want to
include the module in, and it will append its features to all of its descendents:
class Creature < ApplicationRecord
include CreatureConcern
include_recursively CreatureSelfIdentification
endInstallation
Add this line to your application's Gemfile:
gem 'active_record_include'Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/TylerRick/active_record_include.