---
title: 'A JavaScript developer''s guide to Rails: What is Polymorphic Rails Model?'
teaser: Learn how Rails polymorphic associations work under the hood, from database
  schema to model setup, with clear examples for JavaScript developers new to ActiveRecord.
tags: rails,polymorphism,activerecord
author: Will Larry
published_on: 2026-02-11
---

Coming from JavaScript, I'm used to flexible object relationships. Need comments on different things? Add a `commentableId` and `commentableType` field, write some conditional logic, done. Rails has the same pattern, but codifies it into the framework: [**polymorphic associations**](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations).

The database columns work identically to what you'd do in MongoDB or PostgreSQL with a JavaScript backend. What confused me wasn't the columns themselves—it was the Rails magic layered on top. When I saw `belongs_to :commentable, polymorphic: true` in a model, I had no idea how Rails knew which table to query. There's no import, no explicit configuration, just a symbol and an option.

This article walks through the database schema and Rails internals so you can understand what's actually happening.

## The Problem Polymorphic Associations Solve

You're building a social app where users can comment on Posts, Photos, and Videos.

### The JavaScript Approach

In JavaScript with MongoDB, you might do:

```javascript
// Comment document
{
  id: 1,
  body: "Great post!",
  userId: 42,
  commentableType: "Post",  // or "Photo" or "Video"
  commentableId: 123
}

// Then when fetching:
async function getCommentable(comment) {
  if (comment.commentableType === "Post") {
    return await Post.findById(comment.commentableId);
  } else if (comment.commentableType === "Photo") {
    return await Photo.findById(comment.commentableId);
  }
  // ... more conditionals
}
```

This works, but you're writing the same conditional logic everywhere you need to load the parent.

### The Rails Way

Rails bakes this pattern into the framework:

```ruby
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

class Post < ApplicationRecord
  has_many :comments, as: :commentable
end
```

Now:

```ruby
comment = Comment.first
comment.commentable  # Rails automatically loads the right model

post = Post.first
post.comments  # All comments on this post
```

No conditionals. No manual lookups. Rails reads the `_type` column and calls the right model.

## How Polymorphic Associations Actually Work

### The Database Schema

A polymorphic association requires two columns:

```ruby
# db/migrate/20250101000000_create_comments.rb
class CreateComments < ActiveRecord::Migration[7.0]
  def change
    create_table :comments do |t|
      t.text :body
      t.references :user, null: false, foreign_key: true

      # Polymorphic association - creates both columns
      t.references :commentable, polymorphic: true, null: false

      t.timestamps
    end

    add_index :comments, [:commentable_type, :commentable_id]
  end
end
```

This creates:

- `commentable_type` (string) - stores the model class name: "Post", "Photo", "Video"
- `commentable_id` (bigint) - stores the ID of that record

Polymorphic associations can't have database [foreign key constraints](https://thoughtbot.com/blog/referential-integrity-with-foreign-keys) because `commentable_id` could point to multiple tables. The database can't enforce referential integrity across tables, so Rails handles validation at the application level.

### Creating a Comment

```ruby
post = Post.find(123)
comment = post.comments.create(body: "Great post!", user: current_user)
```

Rails automatically sets the polymorphic columns:

```sql
INSERT INTO comments (body, user_id, commentable_type, commentable_id, ...)
VALUES ('Great post!', 42, 'Post', 123, ...)
```

### Loading the Commentable

```ruby
comment = Comment.first
comment.commentable  # What happens here?
```

Rails reads `commentable_type` (gets "Post"), reads `commentable_id` (gets 123), then calls `Post.find(123)`:

```ruby
# What Rails does internally:
class_name = comment.commentable_type  # => "Post"
record_id = comment.commentable_id     # => 123
class_name.constantize.find(record_id) # => Post.find(123)
```

## Setting Up Your Models

### On the "Many" Side (Comment)

```ruby
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
  belongs_to :user

  validates :body, presence: true
end
```

The `polymorphic: true` option tells Rails to check `commentable_type` to determine which model to load.

### On the "One" Side (Post, Photo, Video)

```ruby
class Post < ApplicationRecord
  has_many :comments, as: :commentable, dependent: :destroy
end

class Photo < ApplicationRecord
  has_many :comments, as: :commentable, dependent: :destroy
end
```

The `as: :commentable` must match the name in `belongs_to`. The `dependent: :destroy` deletes comments when the parent is deleted.

## Building a Commenting System

### Generate the migration

```bash
rails generate model Comment body:text user:references commentable:references{polymorphic}
```

### Set up routes with [concerns](https://guides.rubyonrails.org/routing.html#routing-concerns)

```ruby
# config/routes.rb
Rails.application.routes.draw do
  concern :commentable do
    resources :comments, only: [:create, :destroy]
  end

  resources :posts, concerns: :commentable
  resources :photos, concerns: :commentable
  resources :videos, concerns: :commentable
end
```

### Create the controller with Signed Global IDs

Instead of passing `commentable_type` and `commentable_id` as separate params (which requires whitelisting types and constantizing strings), use Rails' [Signed Global IDs](https://github.com/rails/globalid). They encode the model type and ID into a single tamper-proof token:

```ruby
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_commentable

  def create
    @comment = @commentable.comments.build(comment_params)
    @comment.user = current_user

    if @comment.save
      redirect_to @commentable, notice: "Comment created!"
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_commentable
    @commentable = GlobalID::Locator.locate_signed(params[:commentable_sgid])
    raise ActiveRecord::RecordNotFound unless @commentable
  end

  def comment_params
    params.require(:comment).permit(:body)
  end
end
```

In your form, generate the signed global ID:

```erb
<%= form_with model: Comment.new, url: comments_path do |f| %>
  <%= hidden_field_tag :commentable_sgid, @post.to_sgid.to_s %>
  <%= f.text_area :body %>
  <%= f.submit %>
<% end %>
```

This approach prevents users from tampering with params to comment on records they shouldn't access. For more on this pattern, see [Let Rails Help You](https://thoughtbot.com/blog/let-rails-help-you).

## Common Polymorphic Patterns

### DRY with Concerns

Extract the association into a concern when multiple models share the same polymorphic relationship:

```ruby
# app/models/concerns/commentable.rb
module Commentable
  extend ActiveSupport::Concern

  included do
    has_many :comments, as: :commentable, dependent: :destroy
  end
end

# app/models/post.rb
class Post < ApplicationRecord
  include Commentable
end
```

### Querying

```ruby
# Get all comments on posts
Comment.where(commentable_type: "Post")

# Eager load to avoid N+1
Comment.includes(:commentable).where(user: current_user)

# Count by type
Comment.group(:commentable_type).count
# => {"Post" => 150, "Photo" => 89, "Video" => 45}
```

If you're using [delegated types](https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html) (Rails 6.1+), you get scopes like `Comment.posts` for free.

## Common Pitfalls

### Forgetting the Index

Always add a composite index on both columns:

```ruby
add_index :comments, [:commentable_type, :commentable_id]
```

### Mismatched Names

The `as:` option must match the `belongs_to` name exactly:

```ruby
# These must match
belongs_to :commentable, polymorphic: true
has_many :comments, as: :commentable  # not :post_commentable
```

### N+1 Queries

```ruby
# Triggers N+1
comments.each { |c| c.commentable.title }

# Fixed
Comment.includes(:commentable).each { |c| c.commentable.title }
```

## When NOT to Use Polymorphic Associations

Avoid polymorphic associations when:

- You need database foreign key constraints (polymorphic can't have them)
- You only have 2-3 parent models (separate foreign keys are simpler)
- The parent models have very different behavior

Alternatives: separate associations with foreign keys, Single Table Inheritance (STI) if models are similar, or [delegated types](https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html) (Rails 6.1+).

## Wrapping up

Polymorphic associations encode a common pattern—one model belonging to multiple types—into Rails conventions. The database stores a `_type` and `_id` column, and Rails handles the rest.

The key points:

- Two columns: `{name}_type` (string) and `{name}_id` (bigint)
- `belongs_to :name, polymorphic: true` on the child
- `has_many :children, as: :name, dependent: :destroy` on each parent
- Always index `[:type_column, :id_column]`
- Use Signed Global IDs in controllers to avoid constantizing user input
- No database foreign keys possible—Rails handles integrity at the application level
