Relationships¶
Dynorm provides Laravel Eloquent-style relationships for modeling entity connections in DynamoDB.
Relationship Types¶
| Type | Description | Example |
|---|---|---|
HasOne | One-to-one, FK on related | User has one Profile |
HasMany | One-to-many, FK on related | User has many Posts |
BelongsTo | Inverse of HasOne/HasMany | Post belongs to User |
BelongsToMany | Many-to-many via pivot | User has many Roles |
HasManyThrough | Has many through intermediate | Country has Posts through Users |
HasOne¶
One-to-one relationship where the foreign key is on the related entity:
type User struct {
dynorm.Entity
Email string
Username string
// Relationship field
Profile HasOne[*ProfileRepository] `dynorm:"hasOne:Profile,UserID"`
}
type Profile struct {
dynorm.Entity
UserID string `dynorm:"gsi:ByUser"` // Foreign key
Bio string
Avatar string
}
Usage¶
user, _ := UserRepo.Find("01HQ3K...")
// Get related profile (lazy loaded)
profile, err := user.Profile.Get()
// With soft delete handling
profile, err := user.Profile.WithTrashed().Get()
HasMany¶
One-to-many relationship where the foreign key is on the related entities:
type User struct {
dynorm.Entity
Email string
// Relationship field
Posts HasMany[*PostRepository] `dynorm:"hasMany:Post,AuthorID"`
}
type Post struct {
dynorm.Entity
AuthorID string `dynorm:"gsi:ByAuthor"` // Foreign key
Title string
Content string
}
Usage¶
user, _ := UserRepo.Find("01HQ3K...")
// Get all posts
posts, err := user.Posts.Get()
// Count posts
count, err := user.Posts.Count()
// Add a new post
newPost := &Post{Title: "Hello", Content: "World"}
err := user.Posts.Add(newPost)
// newPost.AuthorID is automatically set to user.ID
// Remove a post (clears FK, doesn't delete)
err := user.Posts.Remove(existingPost)
BelongsTo¶
Inverse of HasOne/HasMany where the foreign key is on this entity:
type Post struct {
dynorm.Entity
AuthorID string `dynorm:"gsi:ByAuthor"` // Foreign key
Title string
// Relationship field
Author BelongsTo[*UserRepository] `dynorm:"belongsTo:User,AuthorID"`
}
Usage¶
BelongsToMany¶
Many-to-many relationship using a pivot table:
type User struct {
dynorm.Entity
Email string
// Many-to-many with roles
Roles BelongsToMany[*RoleRepository] `dynorm:"belongsToMany:Role"`
}
type Role struct {
dynorm.Entity
Name string
Permissions []string
// Inverse relationship
Users BelongsToMany[*UserRepository] `dynorm:"belongsToMany:User"`
}
Usage¶
user, _ := UserRepo.Find("01HQ3K...")
// Get all roles
roles, err := user.Roles.Get()
// Count roles
count, err := user.Roles.Count()
// Attach a role
adminRole, _ := RoleRepo.Find("01HQ4L...")
err := user.Roles.Attach(adminRole)
// Attach with pivot data
err := user.Roles.AttachWithContext(ctx, adminRole, map[string]any{
"granted_by": "system",
"expires_at": time.Now().AddDate(1, 0, 0),
})
// Detach a role
err := user.Roles.Detach(adminRole)
// Sync roles (replace all)
err := user.Roles.Sync([]any{role1, role2, role3})
// Toggle (attach if not attached, detach if attached)
err := user.Roles.Toggle(someRole)
HasManyThrough¶
Access distant relations through an intermediate entity:
type Country struct {
dynorm.Entity
Name string
// Has many posts through users
Posts HasManyThrough[*PostRepository, *UserRepository] `dynorm:"hasManyThrough:Post,User,CountryID,AuthorID"`
}
type User struct {
dynorm.Entity
CountryID string `dynorm:"gsi:ByCountry"`
Email string
}
type Post struct {
dynorm.Entity
AuthorID string `dynorm:"gsi:ByAuthor"`
Title string
}
Usage¶
country, _ := CountryRepo.Find("01HQ3K...")
// Get all posts from users in this country
posts, err := country.Posts.Get()
count, err := country.Posts.Count()
Eager Loading¶
Prevent N+1 queries by eager loading relationships:
// Load users with their posts
users, err := UserRepo.
With("Posts").
GetAll()
// Access pre-loaded posts (no additional query)
for _, user := range users.Items() {
posts, _ := user.Posts.Get() // Already loaded
}
// Load multiple relationships
users, err := UserRepo.
With("Posts", "Profile", "Roles").
GetAll()
Soft Deletes in Relationships¶
Handle soft-deleted related entities:
// Exclude soft deleted (default)
posts, _ := user.Posts.Get()
// Include soft deleted
posts, _ := user.Posts.WithTrashed().Get()
// Exclude again
posts, _ := user.Posts.WithoutTrashed().Get()
Relationship Diagram¶
erDiagram
User ||--o| Profile : "has one"
User ||--o{ Post : "has many"
Post }o--|| User : "belongs to"
User }o--o{ Role : "belongs to many"
Country ||--o{ User : "has many"
Country ||--o{ Post : "has many through" Pivot Table¶
BelongsToMany relationships use an automatic pivot table:
| Column | Description |
|---|---|
pk | {pivot_key}#{left_id} |
sk | {right_id} |
pivot_key | Relationship identifier |
id_left | Left entity ID |
id_right | Right entity ID |
created_at | Relationship created timestamp |
extra | Custom pivot attributes |
The pivot table is automatically managed by Dynorm.
Pivot Data¶
Access extra data stored on the pivot:
roles, _ := user.Roles.Get()
// Get pivot data for each relationship
pivotData := user.Roles.GetPivotData()
for i, data := range pivotData {
fmt.Printf("Role added at: %s\n", data.CreatedAt)
if grantedBy, ok := data.Extra["granted_by"]; ok {
fmt.Printf("Granted by: %s\n", grantedBy)
}
}
Best Practices¶
Do
- Use GSIs on foreign key fields
- Eager load when accessing relationships for multiple entities
- Use appropriate relationship type for your access pattern
- Consider soft deletes for audit trails
Don't
- Create circular eager loading
- Forget to index foreign keys
- Use BelongsToMany for simple one-to-many
Relationship Type Selection¶
flowchart TD
Start[Relationship Needed] --> Q1{How many on<br>each side?}
Q1 -->|One-to-One| Q2{FK on which<br>entity?}
Q1 -->|One-to-Many| Q3{FK on which<br>entity?}
Q1 -->|Many-to-Many| BelongsToMany[BelongsToMany]
Q2 -->|Related entity| HasOne[HasOne]
Q2 -->|This entity| BelongsTo[BelongsTo]
Q3 -->|Related entities| HasMany[HasMany]
Q3 -->|This entity| BelongsTo Next Steps¶
- Morph Relationships - Polymorphic relationships
- Eager Loading - Prevent N+1 queries
- Query Builder - Query with relationships