STI with polymorphism

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?

Isn’t Blog.presence calling the Object#presence method, rather than referring to a has_one association? Object

1 Like

Hmm, you’re right. Not sure if that’s the issue, but I should change that anyway. Thanks for pointing it out!

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.

Rails API documentation states:

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.

Polymorphic Associations