AL+

Language: RO (română) EN (English)

Email us: paul@alplustech.com

Looking back at the week of 21–27 May 2025

Rails polymorphism needs explicit guarantees before the schema spreads

Rails polymorphic associations simplify shared relationships. Define allowed targets, deletion rules and database guarantees before more tables depend on them.

Citiți articolul în română

A polymorphic association is easy to add and harder to unwind after several features depend on it. Choose it with the integrity contract visible: which target types are allowed, what deletion means and which guarantees the database can enforce. A convenient model declaration should not become the only explanation of a relationship that billing, exports or background jobs later treat as permanent fact.

Looking back from November 2026, the 23 May 2025 Rails report highlighted two relevant changes. One documented an existing restriction on polymorphic associations used as a through association. The other described deprecating class_name on polymorphic belongs_to. These were separate signals about the model’s limits, not a newly introduced database integrity system.

The type column does real work

A conventional relationship identifies a known target table. A polymorphic relationship instead stores both an identifier and a type used to resolve the target model. The Rails 8.0 association guide, consulted for this retrospective, explains that pattern. Its version label supplies context; it is not a publication timestamp for every sentence now present on the page.

Consider an illustrative product in which a note can belong to either a project or a shipment. Sharing the notes table can make the common behavior straightforward. It also means that the identifier alone does not explain what the note belongs to. The type is part of the relationship’s meaning, and changes to model names or permitted targets deserve data-migration planning.

The 20 May pull request making class_name invalid on main explains why that option is misplaced here: the associated class comes from the type column at runtime. The separate deprecation pull request, merged later that day, targeted 8-0-stable. A declaration containing an extra class name should not be mistaken for an effective target restriction.

That branch distinction matters. The weekly report described the deprecation and the intended Rails 8.1 removal, while the underlying records distinguish invalidity on main from a warning on the stable branch. A merged change is not proof that every installed Rails version already behaved that way. Check the application’s actual version before assigning a warning or error to a release.

Association traversal and integrity are different questions

The documentation pull request merged on 23 May clarified that a polymorphic association cannot itself serve as the through association. It documented an existing error condition. That does not mean every association combining through and polymorphism is forbidden: a polymorphic source can be disambiguated with source_type. The position of the polymorphic relationship matters.

This is an association-resolution constraint. Database integrity is a separate matter. An ordinary foreign key refers to a particular target table; a single type-and-id pair naming several tables does not automatically gain the same referential guarantee. An index can make lookup efficient without proving that the referenced row exists. Do not present those mechanisms as interchangeable.

Application validations can reject invalid values through the paths that run them. They should not be described as a complete database guarantee when bulk imports, direct SQL or other writers can bypass that path. Concurrency can matter too: checking that a target exists and later storing a relationship are separate operations unless a suitable mechanism binds them.

A project can choose stronger custom enforcement, but that is additional design with its own maintenance cost. The article does not prescribe a portable cross-table foreign key or supply an untested migration. The first decision is which invariants actually require enforcement and where every writer will meet them.

Settle deletion before adding the next consumer

For the proposed notes feature, decide whether deleting a shipment should delete its notes, preserve an audit record or prevent deletion. Those are different product rules. A callback on one model is not an explanation of what happens when a maintenance job or external writer removes the target. Document the complete write and deletion paths that the product supports.

Tenant boundaries need an equally explicit rule. A valid project identifier does not establish that the current user may attach a note to it. Resolve the target within the authorized scope and retain authorization at the operation boundary. Relationship convenience must not turn a supplied type and identifier into permission to reach an unrelated customer’s data.

The business cost of leaving these questions open grows as the schema spreads. An export might assume every note has a live target; an audit feature might require preserved historical meaning. Changing the relationship later can require moving data and coordinating readers. These are possible consequences of the proposed design, not reported failures or measured migration costs.

Before extending the pattern, ask each new consumer which assumption it needs. If the answers differ, shared storage may conceal separate domain relationships. If they agree, a bounded polymorphic model with documented writers and deletion rules can remain clear. The number of model classes alone does not decide the issue.

Separate tables can also become unnecessary machinery

The strongest counterargument is that splitting every target into its own relationship table can multiply sparse tables and repetitive code. For a small, stable set of targets with shared lifecycle rules, polymorphism may be the simpler model. Database purity alone is a weak reason to introduce a more complicated application.

A conventional join model is attractive when its known target tables permit straightforward constraints and its domain meaning is distinct. A polymorphic relation is attractive when the shared behavior is real and its weaker default guarantees are acceptable or deliberately supplemented. Compare those concrete alternatives before committing dependent features to either structure.

For custom software development, retain a short relationship contract alongside the model: allowed targets, authorization, deletion and enforcement. The May Rails changes made particular limits clearer. The application still has to decide which promises belong in its association declarations and which must survive every path into the database.