Notes /

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.