Modifying polymorphic type in Rails
Polymorphic associations in Rails allow an entity to be associated with more than one other entity through a single association. This flexibility is especially useful when generic entities like comments or files need to be linked to multiple other entities. Without a polymorphic association, you'd typically resort to creating multiple junction tables (e.g., article_comments, video_comments, image_comments) for each type of association, which can clutter your database schema.
Setting Up a Polymorphic Association
To set up a polymorphic association in Rails, you can use the rails generate
command to create a model. For instance, let's create a Topping
model with a polymorphic association:
rails generate model Topping name:string toppable:references{polymorphic}
In the generated migration file, you will notice that the toppable
reference is declared as polymorphic:
class CreateToppings < ActiveRecord::Migration[7.0]
def change
create_table :toppings do |t|
t.references :toppable, polymorphic: true, null: false
t.string :name
t.timestamps
end
end
end
Understanding the Migration
When you run rails db:migrate
, Rails will add two new columns to your toppings
table: toppable_type
and toppable_id
. toppable_type
stores the class name of the associated entity (e.g., Entrees::Pizza
, Sandwich
), while toppable_id
stores the corresponding ID.
Updating Your Models
Update your models to reflect the polymorphic association:
class Topping < ApplicationRecord
belongs_to :toppable, polymorphic: true
end
class Pizza < ApplicationRecord
has_many :toppings, as: :toppable
end
Working with Polymorphic Associations
Now, you can associate toppings with different types of entities seamlessly:
pizza = Pizza.find(pizza_id)
new_pizza_topping = Topping.new(toppable: pizza, name: 'Chicken tikka masala')
Customizing polymorphic_type
By default, Rails uses the class name Pizza
or Sandwich
as toppable_type
. If your models are classified in modules (e.g. Entrees::Pizza
), your toppable_type
would be Entrees::Pizza
and you might not want that.
If you want to customize this behavior, for example, to use a different identifier like pizza
, you can override the polymorphic_name
method in your model:
module Entrees
class Pizza < ApplicationRecord
# ...
def self.polymorphic_name = 'pizza'
end
end
Handling Custom Polymorphic Types
To ensure Rails can correctly map your custom polymorphic type to the correct model class, you'll need to define a mapping in your Topping
model:
class Topping < ApplicationRecord
# ...
TOPPABLE_CLASS_LOOKUP = {
'pizza' => Entrees::Pizza,
'sandwich' => Sandwich
}.freeze
def self.polymorphic_class_for(name)
TOPPABLE_CLASS_LOOKUP[name] || super(name)
end
end
This mapping ensures that Rails can locate the correct model class based on the toppable_type
stored in your database.
If you skip this step, you'll get a NameError: wrong constant name pizza
from your Topping
model
What's the point?
Using human-readable labels or identifiers for polymorphic types instead of technical class names ensures control over how data is exposed from your system and allows flexibility to move or rename classes and modules without having to modify existing database records.