Skip to content

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

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

// Get the author
author, err := post.Author.Get()

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