Polymorphic

A polymorphic relationship is where a model can belong to more than one type of model on a single association.

One To One

A one-to-one polymorphic relation is similar to a simple one-to-one relation; however, the target model can belong to more than one type of model on a single association. For example, an Image might be associated with a User or Post model.

Defining A One To One Polymorphic Relationship

To define this relationship, for example, a User or Post model might be associated with one Image, we define a morphOne field to the User and Post models.

class Image extends Model {
  static entity = 'images'
  static fields () {
    return {
      id: this.number(0),
      url: this.string(''),
      imageableId: this.number(0),
      imageableType: this.string(''),
    }
  }
}
class User extends Model {
  static entity = 'users'
  static fields () {
    return {
      id: this.number(0),
      name: this.string(''),
      image: this.morphOne(Image, 'imageableId', 'imageableType')
    }
  }
}
class Post extends Model {
  static entity = 'posts'
  static fields () {
    return {
      id: this.number(0),
      title: this.string(''),
      image: this.morphOne(Image, 'imageableId', 'imageableType')
    }
  }
}

The first argument passed to the morphOne method is the name of the model, the second argument is the name of the field which will contain the id of the model, and the third argument is the name of the field which will contain the entity of the parent model. The third argument is used to determine the "type" of the related parent model.

Additionally, Pinia ORM assumes that the foreign key should have a value matching the id (or the custom static primaryKey) field of the parent. In other words, Pinia ORM will look for the value of the user's id field in the imageableId field of the Image record. If you would like the relationship to use a value other than id, you may pass a fourth argument to the morphOne method specifying your custom key:

class User extends Model {
  static entity = 'users'
  static fields () {
    return {
      id: this.number(0),
      userId: this.string(''),
      name: this.string(''),
      image: this.morphOne(Image, 'imageableId', 'imageableType', 'userId')
    }
  }
}

Defining The Inverse Of The Relationship

So, we can access the Image model from our User or Post. Now, let's define a relationship on the Image model that will let us access the model which owns the image. We can define the inverse of a morphOne relationship using the morphTo attribute:

class Image extends Model {
  static entity = 'images'
  static fields () {
    return {
      id: this.number(0),
      url: this.string(''),
      imageableId: this.number(0),
      imageableType: this.string(''),
      imageable: this.morphTo(
        [User, Post],
        'imageableId',
        'imageableType'
      )
    }
  }
}

The first argument passed to the morphTo method is an array of models which are related, the second argument is the name of the field which will contain the id of the model, and the third argument is the name of the field which will contain the entity of the related model. The third argument is used to determine the "type" of the related model. You may also pass a fourth argument to the morphTo method specifying your custom key on the related model.

class Image extends Model {
  static entity = 'images'
  static fields () {
    return {
      id: this.number(0),
      url: this.string(''),
      imageableId: this.number(0),
      imageableType: this.string(''),
      imageable: this.morphTo(
        [User, Post],
        'imageableId',
        'imageableType',
        'morphableId'
      )
    }
  }
}

One To Many

A one-to-many polymorphic relation is similar to a simple one-to-many relation; however, the target model can belong to more than one type of model on a single association. For example, a Comment might be associated with a Post or Video model.

Defining A One To Many Polymorphic Relationship

To define this relationship, for example, a Post or Video model might be associated with one or more Comment(s), we define a morphMany field to the Post and Video models.

class Comment extends Model {
  static entity = 'comments'
  static fields () {
    return {
      id: this.number(0),
      url: this.string(''),
      commentableId: this.number(0),
      commentableType: this.string(''),
    }
  }
}
class Video extends Model {
  static entity = 'videos'
  static fields () {
    return {
      id: this.number(0),
      link: this.string(''),
      comments: this.morphMany(Comment, 'commentableId', 'commentableType')
    }
  }
}
class Post extends Model {
  static entity = 'posts'
  static fields () {
    return {
      id: this.number(0),
      title: this.string(''),
      comments: this.morphMany(Comment, 'commentableId', 'commentableType')
    }
  }
}

The first argument passed to the morphMany method is the name of the model, the second argument is the name of the field which will contain the id of the model, and the third argument is the name of the field which will contain the entity of the parent model. The third argument is used to determine the "type" of the related parent model.

Additionally, Pinia ORM assumes that the foreign key should have a value matching the id (or the custom static primaryKey) field of the parent. In other words, Pinia ORM will look for the value of the video's id field in the commentableId field of the Comment record. If you would like the relationship to use a value other than id, you may pass a fourth argument to the morphMany method specifying your custom key:

class Video extends Model {
  static entity = 'videos'
  static fields () {
    return {
      id: this.number(0),
      videoId: this.string(''),
      link: this.string(''),
      comments: this.morphMany(Comment, 'commentableId', 'commentableType', 'videoId')
    }
  }
}

Many To Many (Polymorphic)

In addition to traditional polymorphic relations, you may also define "many-to-many" polymorphic relations. For example, a blog Post and Video model could share a polymorphic relation to a Tag model. Using a many-to-many polymorphic relation allows you to have a single list of unique tags that are shared across blog posts and videos.

You can define many-to-many polymorphic relations by using the this.morphToMany attribute.

class Post extends Model {
  static entity = 'posts'
  static fields () {
    return {
      id: this.attr(null),
      tags: this.morphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type')
    }
  }
}
class Video extends Model {
  static entity = 'videos'
  static fields () {
    return {
      id: this.attr(null),
      tags: this.morphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type')
    }
  }
}
class Tag extends Model {
  static entity = 'tags'
  static fields () {
    return {
      id: this.attr(null),
      name: this.attr('')
    }
  }
}
class Taggable extends Model {
  static entity = 'taggables'
  static primaryKey = ['tag_id', 'taggable_id', 'taggable_type']
  static fields () {
    return {
      id: this.attr(null),
      tag_id: this.attr(null),
      taggable_id: this.attr(null),
      taggable_type: this.attr(null)
    }
  }
}

Defining The Inverse Of The Relationship

To define the inverse relation to fetch related record – for this example it's for Tag model – you can use the this.morphedByMany() attribute.

class Tag extends Model {
  static entity = 'tags'
  static fields () {
    return {
      id: this.attr(null),
      name: this.attr(''),
      posts: this.morphedByMany(
        Post, Taggable, 'tag_id', 'taggable_id', 'taggable_type'
      ),
      videos: this.morphedByMany(
        Video, Taggable, 'tag_id', 'taggable_id', 'taggable_type'
      )
    }
  }
}

Access Intermediate Model

As the same as belongsToMany relationship, you may access the intermediate model for polymorphic many-to-many relationship through pivot attribute on the model.

const post = Post.query().with('tags').first()
post.tags.forEach((tag) => {
  console.log(tag.pivot)
})

Each Tag model we retrieve is automatically assigned a pivot attribute. This attribute contains a model representing the intermediate model and may be used like any other model.