Skip to main content

What Are Polymorphic Relations?

A polymorphic relation lets a single model belong to more than one other model using the same association. The classic example: comments that can belong to either a Post or a Video — without needing a separate post_comments and video_comments table.

How It Works

Two special columns store the type and ID of the parent:
ColumnExample Value
commentable_type'Post' or 'Video'
commentable_id42

1. Migration

export async function up(schema) {
  await schema.createTable('comments', (table) => {
    table.increments('id');
    table.integer('user_id').unsigned().references('id').inTable('users');
    table.string('commentable_type').notNullable();  // 'Post' or 'Video'
    table.integer('commentable_id').unsigned().notNullable();
    table.text('body').notNullable();
    table.timestamps(true, true);
    table.index(['commentable_type', 'commentable_id']);
  });
}

2. Models

Comment — the “child” side, uses morphTo:
// models/Comment.js
import Model from 'ilana-orm/orm/Model.js';

export default class Comment extends Model {
  static table = 'comments';
  static fillable = ['user_id', 'commentable_type', 'commentable_id', 'body'];

  static { this.register(); }

  // Belongs to either a Post or a Video
  commentable() {
    return this.morphTo('commentable_type', 'commentable_id');
  }

  user() {
    return this.belongsTo('User', 'user_id');
  }
}
Post and Video — the “parent” sides, use morphMany:
// models/Post.js
import Model from 'ilana-orm/orm/Model.js';

export default class Post extends Model {
  static table = 'posts';
  static { this.register(); }

  comments() {
    return this.morphMany('Comment', 'commentable_type', 'commentable_id');
  }
}
// models/Video.js
import Model from 'ilana-orm/orm/Model.js';

export default class Video extends Model {
  static table = 'videos';
  static { this.register(); }

  comments() {
    return this.morphMany('Comment', 'commentable_type', 'commentable_id');
  }
}

3. Creating Polymorphic Records

const post = await Post.find(1);

// Create a comment on a post
await Comment.create({
  user_id: req.user.id,
  commentable_type: 'Post',
  commentable_id: post.id,
  body: 'Great article!',
});

const video = await Video.find(5);

// Create a comment on a video
await Comment.create({
  user_id: req.user.id,
  commentable_type: 'Video',
  commentable_id: video.id,
  body: 'Loved this video!',
});

4. Querying

Load comments for a post:
const post = await Post.query().with('comments.user').find(1);
post.comments; // all comments on this post
Load what a comment belongs to:
const comment = await Comment.query().with('commentable').find(42);
comment.commentable; // either a Post or a Video instance
Load recent comments across all types:
const recent = await Comment.query()
  .with('commentable', 'user')
  .orderBy('created_at', 'desc')
  .limit(10)
  .get();

recent.forEach(comment => {
  console.log(`${comment.user.name} commented on a ${comment.commentable_type}`);
});

5. morphOne — Single Polymorphic Child

Use morphOne when only one child record exists per parent — for example, each post has one FeaturedImage:
// models/FeaturedImage.js
export default class FeaturedImage extends Model {
  static table = 'featured_images';
  static { this.register(); }

  imageable() {
    return this.morphTo('imageable_type', 'imageable_id');
  }
}

// models/Post.js
featuredImage() {
  return this.morphOne('FeaturedImage', 'imageable_type', 'imageable_id');
}
const post = await Post.query().with('featuredImage').find(1);
post.featuredImage; // single FeaturedImage or null

Common Use Cases

Polymorphic relationExample
Comments on posts and videosmorphMany / morphTo
Tags on any modelmorphMany / morphTo
Likes on posts, comments, videosmorphMany / morphTo
Featured image for posts and productsmorphOne / morphTo
Notifications for any actormorphMany / morphTo

Important: Register Your Models

Polymorphic resolution works by looking up model classes by name in the registry. Make sure every model involved in the relation has static { this.register(); }:
export default class Post extends Model {
  static { this.register(); } // ← required for morphTo resolution
}