Skip to content

Morph Relationships

Polymorphic (morph) relationships allow an entity to belong to multiple entity types using a single relationship definition.

Overview

Use morph relationships when:

  • A model can belong to multiple different models
  • You want to avoid creating multiple relationship fields
  • The same related content applies to different parent types

Morph Relationship Types

Type Description Example
MorphOne Polymorphic one-to-one User/Post has one Image
MorphMany Polymorphic one-to-many User/Post has many Comments
MorphToMany Polymorphic many-to-many User/Post has many Tags
MorphedByMany Inverse of MorphToMany Tag morphed by Users and Posts

MorphOne

A polymorphic one-to-one relationship:

type User struct {
    dynorm.Entity
    Email string

    // User can have one image
    Avatar MorphOne[*ImageRepository] `dynorm:"morphOne:Image,imageable"`
}

type Post struct {
    dynorm.Entity
    Title string

    // Post can have one image
    FeaturedImage MorphOne[*ImageRepository] `dynorm:"morphOne:Image,imageable"`
}

type Image struct {
    dynorm.Entity

    // Polymorphic fields
    ImageableType string `dynorm:"gsi:ByImageable"` // "user" or "post"
    ImageableID   string `dynorm:"gsi:ByImageable:sk"`

    URL    string
    Width  int
    Height int
}

Usage

user, _ := UserRepo.Find("01HQ3K...")

// Get user's avatar
avatar, err := user.Avatar.Get()

post, _ := PostRepo.Find("01HQ4L...")

// Get post's featured image
featuredImage, err := post.FeaturedImage.Get()

MorphMany

A polymorphic one-to-many relationship:

type User struct {
    dynorm.Entity
    Email string

    // User can have many comments (on their profile)
    Comments MorphMany[*CommentRepository] `dynorm:"morphMany:Comment,commentable"`
}

type Post struct {
    dynorm.Entity
    Title string

    // Post can have many comments
    Comments MorphMany[*CommentRepository] `dynorm:"morphMany:Comment,commentable"`
}

type Comment struct {
    dynorm.Entity

    // Polymorphic fields
    CommentableType string `dynorm:"gsi:ByCommentable"`
    CommentableID   string `dynorm:"gsi:ByCommentable:sk"`

    AuthorID string
    Content  string
}

Usage

post, _ := PostRepo.Find("01HQ3K...")

// Get all comments on post
comments, err := post.Comments.Get()

// Count comments
count, err := post.Comments.Count()

// Add a comment
newComment := &Comment{AuthorID: userID, Content: "Great post!"}
err := post.Comments.Add(newComment)
// CommentableType = "post", CommentableID = post.ID

MorphToMany

A polymorphic many-to-many relationship:

type User struct {
    dynorm.Entity
    Email string

    // User can have many tags
    Tags MorphToMany[*TagRepository] `dynorm:"morphToMany:Tag,taggable"`
}

type Post struct {
    dynorm.Entity
    Title string

    // Post can have many tags
    Tags MorphToMany[*TagRepository] `dynorm:"morphToMany:Tag,taggable"`
}

type Tag struct {
    dynorm.Entity

    Name string `dynorm:"gsi:ByName"`
    Slug string
}

Usage

post, _ := PostRepo.Find("01HQ3K...")

// Get all tags
tags, err := post.Tags.Get()

// Attach a tag
goTag, _ := TagRepo.Where("Name", "=", "golang").GetFirst()
err := post.Tags.Attach(goTag)

// Attach with pivot data
err := post.Tags.AttachWithContext(ctx, goTag, map[string]any{
    "added_by": userID,
})

// Detach
err := post.Tags.Detach(goTag)

// Sync (replace all tags)
err := post.Tags.Sync([]any{tag1, tag2, tag3})

// Toggle
err := post.Tags.Toggle(someTag)

MorphedByMany

The inverse of MorphToMany - find all entities that morph to this one:

type Tag struct {
    dynorm.Entity

    Name string

    // All entities tagged with this tag
    Taggables MorphedByMany[any] `dynorm:"morphedByMany:taggable"`
}

Usage

tag, _ := TagRepo.Find("01HQ3K...")

// Get all entities with this tag (Users, Posts, etc.)
entities, err := tag.Taggables.Get()

// Returns grouped results by entity type
for _, entity := range entities {
    switch e := entity.(type) {
    case *User:
        fmt.Printf("User: %s\n", e.Email)
    case *Post:
        fmt.Printf("Post: %s\n", e.Title)
    }
}

Pivot Table Schema

Morph relationships use the same pivot table as BelongsToMany:

Column Description
pk {morph_name}_{left_entity}#{left_id}
sk {right_id}
pivot_key Morph relationship identifier
entity_name_left Left entity type (e.g., "post")
id_left Left entity ID
entity_name_right Right entity type (e.g., "tag")
id_right Right entity ID
created_at Relationship timestamp
extra Custom pivot data

Relationship Diagram

erDiagram
    User ||--o| Image : "morph one (avatar)"
    Post ||--o| Image : "morph one (featured)"

    User ||--o{ Comment : "morph many"
    Post ||--o{ Comment : "morph many"

    User }o--o{ Tag : "morph to many"
    Post }o--o{ Tag : "morph to many"

Examples

Taggable System

// Entities
type User struct {
    dynorm.Entity
    Email string
    Tags  MorphToMany[*TagRepository] `dynorm:"morphToMany:Tag,taggable"`
}

type Post struct {
    dynorm.Entity
    Title string
    Tags  MorphToMany[*TagRepository] `dynorm:"morphToMany:Tag,taggable"`
}

type Product struct {
    dynorm.Entity
    Name string
    Tags MorphToMany[*TagRepository] `dynorm:"morphToMany:Tag,taggable"`
}

type Tag struct {
    dynorm.Entity
    Name      string
    Taggables MorphedByMany[any] `dynorm:"morphedByMany:taggable"`
}

// Usage
func TagPost(postID, tagName string) error {
    post, err := PostRepo.Find(postID)
    if err != nil {
        return err
    }

    tag, err := TagRepo.Where("Name", "=", tagName).GetFirst()
    if err != nil {
        return err
    }

    if tag == nil {
        tag = &Tag{Name: tagName}
        if err := TagRepo.Save(tag); err != nil {
            return err
        }
    }

    return post.Tags.Attach(tag)
}

Comment System

func AddComment(targetType, targetID, content, authorID string) error {
    comment := &Comment{
        Content:  content,
        AuthorID: authorID,
    }

    switch targetType {
    case "user":
        user, err := UserRepo.Find(targetID)
        if err != nil {
            return err
        }
        return user.Comments.Add(comment)

    case "post":
        post, err := PostRepo.Find(targetID)
        if err != nil {
            return err
        }
        return post.Comments.Add(comment)

    default:
        return fmt.Errorf("unknown target type: %s", targetType)
    }
}

Best Practices

Do

  • Use morph relationships for truly polymorphic associations
  • Register morph maps for type resolution
  • Index polymorphic fields with composite GSI
  • Consider soft deletes for morph entities

Don't

  • Overuse morph relationships when regular relationships work
  • Forget to handle all entity types in MorphedByMany
  • Create complex morph chains

When to Use Morph

flowchart TD
    Start[Need relationship] --> Q1{Same content for<br>multiple models?}
    Q1 -->|Yes| Q2{Many-to-many?}
    Q1 -->|No| Regular[Use regular relationship]

    Q2 -->|Yes| MorphToMany[MorphToMany]
    Q2 -->|No| Q3{One or many?}

    Q3 -->|One| MorphOne[MorphOne]
    Q3 -->|Many| MorphMany[MorphMany]

Next Steps