Relationships

Define hasOne, hasMany, belongsTo, and manyToMany relationships with eager loading in Velocity ORM.

Velocity ORM supports all common relationship types with eager loading and query constraints.

Relationship Types

TypeDescriptionExample
hasOneOne-to-oneUser has one Profile
hasManyOne-to-manyUser has many Posts
belongsToInverse one-to-one/manyPost belongs to User
belongsToManyMany-to-manyPost has many Tags

Defining Relationships

Has One

type User struct {
    orm.Model[User]
    Name    string   `orm:"column:name;type:varchar(255)"`
    Profile *Profile `orm:"relation:hasOne"`
}

type Profile struct {
    orm.Model[Profile]
    UserID uint   `orm:"column:user_id;type:bigint;not_null"`
    Bio    string `orm:"column:bio;type:text"`
    User   *User  `orm:"relation:belongsTo"`
}

Has Many

type User struct {
    orm.Model[User]
    Name  string `orm:"column:name;type:varchar(255)"`
    Posts []Post `orm:"relation:hasMany"`
}

type Post struct {
    orm.Model[Post]
    UserID uint   `orm:"column:user_id;type:bigint;not_null"`
    Title  string `orm:"column:title;type:varchar(255)"`
    User   *User  `orm:"relation:belongsTo"`
}

Belongs To

type Comment struct {
    orm.Model[Comment]
    PostID  uint   `orm:"column:post_id;type:bigint;not_null"`
    UserID  uint   `orm:"column:user_id;type:bigint;not_null"`
    Content string `orm:"column:content;type:text"`
    Post    *Post  `orm:"relation:belongsTo"`
    User    *User  `orm:"relation:belongsTo"`
}

Belongs To Many (Many-to-Many)

type Post struct {
    orm.Model[Post]
    Title string `orm:"column:title;type:varchar(255)"`
    Tags  []Tag  `orm:"relation:belongsToMany;join_table:post_tags"`
}

type Tag struct {
    orm.Model[Tag]
    Name  string `orm:"column:name;type:varchar(100)"`
    Posts []Post `orm:"relation:belongsToMany;join_table:post_tags"`
}

Pivot table migration:

orm.Schema.Create("post_tags", func(table *orm.Table) {
    table.ForeignID("post_id").Constrained().OnDelete("CASCADE")
    table.ForeignID("tag_id").Constrained().OnDelete("CASCADE")
    table.Primary("post_id", "tag_id")
})

Eager Loading

Basic Eager Loading

// Load single relationship
users, err := User{}.With("Posts").Get()

// Load multiple relationships
users, err := User{}.With("Profile", "Posts").Get()

// Access loaded relationship
for _, user := range users {
    fmt.Printf("%s has %d posts\n", user.Name, len(user.Posts))
}

Nested Eager Loading

// Load nested relationships with dot notation
users, err := User{}.With("Posts.Comments").Get()

// Multiple nested
users, err := User{}.With("Posts.Comments", "Posts.Tags", "Profile").Get()

// Access nested data
for _, user := range users {
    for _, post := range user.Posts {
        fmt.Printf("Post: %s has %d comments\n", post.Title, len(post.Comments))
    }
}

Constrained Eager Loading

// Filter related records
users, err := User{}.
    With("Posts", func(q *orm.Query[Post]) {
        q.Where("published = ?", true).
          OrderBy("created_at", "DESC").
          Limit(5)
    }).Get()

// Multiple constraints
users, err := User{}.
    With("Posts", func(q *orm.Query[Post]) {
        q.Where("published = ?", true)
    }).
    With("Profile", func(q *orm.Query[Profile]) {
        q.Select("id", "user_id", "bio")
    }).Get()

Querying Relationships

Has

Query models that have related records:

// Users who have at least one post
users, err := User{}.Has("Posts").Get()

// Users who have more than 5 posts
users, err := User{}.Has("Posts", ">", 5).Get()

// Users who have between 1 and 10 posts
users, err := User{}.Has("Posts", ">=", 1).Has("Posts", "<=", 10).Get()

Where Has

Query with relationship constraints:

// Users who have published posts
users, err := User{}.WhereHas("Posts", func(q *orm.Query[Post]) {
    q.Where("published = ?", true)
}).Get()

// Users who have posts with comments
users, err := User{}.WhereHas("Posts", func(q *orm.Query[Post]) {
    q.Has("Comments")
}).Get()

// Users who have posts tagged with "golang"
users, err := User{}.WhereHas("Posts", func(q *orm.Query[Post]) {
    q.WhereHas("Tags", func(tq *orm.Query[Tag]) {
        tq.Where("name = ?", "golang")
    })
}).Get()

Doesnt Have

Query models without related records:

// Users with no posts
users, err := User{}.DoesntHave("Posts").Get()

// Users without published posts
users, err := User{}.WhereDoesntHave("Posts", func(q *orm.Query[Post]) {
    q.Where("published = ?", true)
}).Get()
user, _ := User{}.Find(1)

// Create related post
post := Post{Title: "New Post", Body: "Content"}
user.Posts().Save(&post)

// Create multiple
posts := []Post{
    {Title: "Post 1", Body: "Content 1"},
    {Title: "Post 2", Body: "Content 2"},
}
user.Posts().SaveMany(posts)
user, _ := User{}.Find(1)

// Create and return
post, err := user.Posts().Create(map[string]any{
    "title": "New Post",
    "body":  "Content",
})

Associate (Belongs To)

post, _ := Post{}.Find(1)
user, _ := User{}.Find(5)

// Set the user for this post
post.User().Associate(user)
post.Save()

// Dissociate
post.User().Dissociate()
post.Save()

Attach/Detach (Many-to-Many)

post, _ := Post{}.Find(1)

// Attach tags
post.Tags().Attach([]uint{1, 2, 3})

// Attach with pivot data
post.Tags().Attach(map[uint]map[string]any{
    1: {"order": 1},
    2: {"order": 2},
})

// Detach specific tags
post.Tags().Detach([]uint{2, 3})

// Detach all
post.Tags().Detach()

// Sync (attach missing, detach removed)
post.Tags().Sync([]uint{1, 4, 5})
user, _ := User{}.Find(1)

// Update all related posts
user.Posts().Update(map[string]any{
    "published": false,
})

// Update with conditions
user.Posts().Where("created_at < ?", lastMonth).Update(map[string]any{
    "archived": true,
})
user, _ := User{}.Find(1)

// Delete all posts
user.Posts().Delete()

// Delete with conditions
user.Posts().Where("published = ?", false).Delete()

// Delete parent with relationships
user.DeleteWith("Posts", "Profile")
user, _ := User{}.Find(1)

// Count related
postCount := user.Posts().Count()

// Count with conditions
publishedCount := user.Posts().Where("published = ?", true).Count()

// Eager load counts
users, _ := User{}.WithCount("Posts", "Comments").Get()
for _, user := range users {
    fmt.Printf("%s: %d posts\n", user.Name, user.PostsCount)
}

Custom Foreign Keys

type Post struct {
    orm.Model[Post]
    AuthorID uint  `orm:"column:author_id;type:bigint;not_null"`
    Author   *User `orm:"relation:belongsTo;foreign_key:author_id;owner_key:id"`
}

type User struct {
    orm.Model[User]
    Name  string `orm:"column:name;type:varchar(255)"`
    Posts []Post `orm:"relation:hasMany;foreign_key:author_id;local_key:id"`
}

Polymorphic Relationships

type Comment struct {
    orm.Model[Comment]
    CommentableID   uint   `orm:"column:commentable_id;type:bigint"`
    CommentableType string `orm:"column:commentable_type;type:varchar(255)"`
    Content         string `orm:"column:content;type:text"`
}

type Post struct {
    orm.Model[Post]
    Title    string    `orm:"column:title;type:varchar(255)"`
    Comments []Comment `orm:"relation:morphMany;morph:commentable"`
}

type Video struct {
    orm.Model[Video]
    Title    string    `orm:"column:title;type:varchar(255)"`
    Comments []Comment `orm:"relation:morphMany;morph:commentable"`
}

Best Practices

  1. Always use eager loading - Use With() to prevent N+1 queries
  2. Constrain eager loads - Only load what you need with query constraints
  3. Index foreign keys - Ensure all foreign key columns are indexed
  4. Use cascading deletes - Set up ON DELETE CASCADE in migrations
  5. Avoid deep nesting - Limit nested eager loading to 2-3 levels
  6. Count efficiently - Use WithCount() instead of loading and counting