In a Rails 8 app I’m building, there’s a model with a polymorphic association to a model stored in a STI table. Here’s a simplified example of the model:
class Presence < ApplicationRecord
belongs_to :publishable, polymorphic: true
end
Then there’s the base class for the STI model:
class Grouping < ApplicationRecord
end
And the subclass:
class Blog < Grouping
has_one :presence, as: :publishable
end
The problem with this setup is that Blog.presence will return nil, even if the record exists. That’s because the presence association looks for the base class rather than the subclass:
SELECT \"groupings.*\" FROM \"groupings\" WHERE \"groupings\".\"type\" = 'Grouping'
After digging through Rails’ source code, I found that overwriting the polymorphic_name on the class solves the issue:
class Blog < Grouping
has_one :presence, as: :publishable
def self.polymorphic_name = name
end
Now the correct class name is used in the query and everything works as expected. The full test suite runs green, and there are no side-effects so far, although it seems odd to me that the polymorphic_name of the base class is used by default.
But how safe is it to do this? Will it possibly break something else in the future? And is there a better approach?
I attempted to reproduce the issue, but the following test was successful.
Is there any prerequisite I might be overlooking?
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails"
# If you want to test against edge Rails replace the previous line with this:
# gem "rails", github: "rails/rails", branch: "main"
gem "sqlite3"
end
require "active_record/railtie"
require "minitest/autorun"
# This connection will do for database-independent bug reports.
ENV["DATABASE_URL"] = "sqlite3::memory:"
class TestApp < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
config.eager_load = false
config.logger = Logger.new($stdout)
config.secret_key_base = "secret_key_base"
config.active_record.encryption.primary_key = "primary_key"
config.active_record.encryption.deterministic_key = "deterministic_key"
config.active_record.encryption.key_derivation_salt = "key_derivation_salt"
end
Rails.application.initialize!
ActiveRecord::Schema.define do
create_table :groupings, force: true do |t|
t.string :type
end
create_table :presences, force: true do |t|
t.integer :publishable_id
t.string :publishable_type
end
end
class Grouping < ActiveRecord::Base
end
class Blog < Grouping
has_one :presence, as: :publishable
end
class Presence < ActiveRecord::Base
belongs_to :publishable, polymorphic: true
end
class BugTest < ActiveSupport::TestCase
def test_association_stuff
blog = Blog.create!
blog.create_presence!
blog = Blog.first
assert blog.presence
assert_equal blog.presence.id, Presence.first.id
assert_equal blog.id, Presence.first.publishable.id
end
end
Thanks for digging deeper into it. Meanwhile, I’ve renamed the :presence association to :publication, which in hindsight is more intuitive anyway. After doing so, the issue went away, and now I can’t reproduce it. So maybe it was a clash with the presence method, perhaps some unexpected side effect? I’m not sure, but everything is working as expected now.
Using polymorphic associations in combination with single table
inheritance (STI) is a little tricky. In order for the associations to
work as expected, ensure that you store the base model for the STI
models in the type column of the polymorphic association.