ActiveRecord
has the ActiveRecord::Base.ignored_columns = %w(some columns to ignore)
method to blocklist
columns that ActiveRecord
loads from the database. Spelunking the ActiveRecord
source, I found no corresponding method that acts as an allowlist
for columns that will be loaded.I found myself reaching for an allowlist
as I started to take advantage of Rails 6's multi-database support. In my context, data from another database that used to be wrapped in a REST endpoint can now be accessed directly. When making this direct access, I want to ignore every attribute except the few I am specifically interested in. Because we’re in a multiple-app environment, we don’t necessarily have control over columns being added or dropped from the secondary database during runtime. We don’t want our application to go down because another team removed an experimental column from their database.To solve this problem with ignored_columns
, we had to enumerate all the existing columns.class Dogs < AnimalsBase
self.ignored_columns = %w(some list of columns that can never be exhaustive because new columns could be added all the time)
end
This suffers from the problem of new columns could be added at any time. The list of ignored columns has to be constantly tended to ensure we’re robust against a runtime error of columns going away.What I wanted to reach for was something likeclass Dogs < AnimalsBase
self.allowed_columns = %w(id and only the exact columns needed)
end
With this approach, we’re robust against the other database adding and removing columns. We still run the risk of encountering runtime errors if one of our exactly-requested columns goes away. I can’t come up with a way to mitigate that risk aside from improving communications between teams.I’m not solid on the name allowed_columns
but it was the first thing I reached for.For the time being, I implemented this with a monkey patch to load_schema!
ActiveRecord::Base.concerning "AllowedColumns" do
included do
self.allowed_columns = [].freeze
end
module ClassMethods
def allowed_columns
if defined?(@allowed_columns)
@allowed_columns
else
superclass.allowed_columns
end
end
def allowed_columns=(columns)
@allowed_columns = columns.map(&:to_s)
end
# copied from from https://github.com/rails/rails/blob/2dea8c29c794bec564a2e69ad715d25024e93932/activerecord/lib/active_record/model_schema.rb#L484-L497
def load_schema!
unless table_name
raise ActiveRecord::TableNotSpecified, "#{self} has no table configured. Set one with #{self}.table_name="
end
@columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns)
@columns_hash = @columns_hash.slice(*allowed_columns) if allowed_columns.present? # This is the additional line
@columns_hash.each do |name, column|
define_attribute(
name,
connection.lookup_cast_type_from_column(column),
default: column.default,
user_provided_default: false
)
end
end
end
end
Is this a feature that others would find useful now that we’re working with first-class multi-database support?