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¶
- Relationships - Standard relationships
- Events - Lifecycle events
- Query Builder - Query morph relations