Skip to content

Relationships

The ORM supports five relationship types: has_many, has_one, belongs_to, belongs_to_many, and morph_many. Each relationship type models a different kind of association between database tables.

Relationship Types Overview

TypeSQL PatternExample
has_manyParent has many childrenA user has many posts
has_oneParent has one childA user has one profile
belongs_toChild belongs to parentA post belongs to a user
belongs_to_manyMany-to-many via pivot tableA post has many tags
morph_manyPolymorphic many-to-manyComments on posts AND videos

Defining Relationships

Define relationships as methods on your model that return a relationship object.

cpp
struct User : model<User> {
    static inline const std::string table = "users";

    // A user has many posts
    auto posts() {
        return has_many<Post>("user_id");
    }

    // A user has one profile
    auto profile() {
        return has_one<Profile>("user_id");
    }
};

struct Post : model<Post> {
    static inline const std::string table = "posts";

    // A post belongs to a user
    auto author() {
        return belongs_to<User>("user_id");
    }

    // A post belongs to many tags (via post_tags pivot)
    auto tags() {
        return belongs_to_many<Tag>("post_tags", "post_id", "tag_id");
    }
};

struct Tag : model<Tag> {
    static inline const std::string table = "tags";
};

A has_many relationship indicates that the current model can have multiple instances of the related model. The related table has a foreign key pointing to this model's primary key.

cpp
// Definition
auto posts() {
    return has_many<Post>("user_id");
    //                  └── foreign key on the posts table
}

// Loading
user->load(pool, user->posts(), [](auto ec, auto posts) {
    for (auto& post : posts) {
        fmt::println("Post: {}", post->title);
    }
});

// Creating a related record
auto posts_rel = user->posts();
posts_rel.create(pool, user->get_key(),
    {"title", "body"},
    {"My Post", "Post content"},
    [](auto ec, auto post) { }
);

Parameters for has_many<Related>(fk, local_key, related_table):

  • fk — Foreign key column name on the related table (e.g., "user_id").
  • local_key — Local key column (default: "id").
  • related_table — Related table name (default: Related::table).

A has_one relationship is like has_many, but the related table can only have one matching row.

cpp
// Definition
auto profile() {
    return has_one<Profile>("user_id");
}

// Loading
user->load(pool, user->profile(), [](auto ec, auto profile) {
    fmt::println("Profile bio: {}", profile->bio);
});

Parameters: Same as has_many.


belongs_to<Parent> — Inverse one-to-many

A belongs_to relationship defines the inverse of has_many or has_one. The current model's table contains the foreign key.

cpp
// Definition
auto author() {
    return belongs_to<User>("user_id");
}

// Loading (requires the foreign key value)
post->load(pool, post->author(), post->user_id, [](auto ec, auto author) {
    fmt::println("Author: {}", author->name);
});

Parameters for belongs_to<Parent>(fk, parent_key):

  • fk — Foreign key column on this table (e.g., "user_id").
  • parent_key — Primary key column on the parent table (default: "id").

A belongs_to_many relationship links two tables through a pivot (junction) table. Each model can have many instances of the related model, and vice versa.

cpp
// Definition
auto tags() {
    return belongs_to_many<Tag>("post_tags", "post_id", "tag_id");
    //                          └── pivot table   └── local FK  └── related FK
}

// Loading
post->load(pool, post->tags(), [](auto ec, auto tags) { });

// Eager loading
q->with_belongs_to_many<Tag>("post_tags", "post_id", "tag_id", "tags");

Attach / Detach / Sync / Toggle

cpp
auto tags_rel = post->tags();

// Attach: add relations
tags_rel.attach(pool, post->get_key(), {1, 2, 3}, callback);

// Detach: remove relations
tags_rel.detach(pool, post->get_key(), callback, {2, 3});

// Sync: match exactly this set (attaches missing, detaches extra)
tags_rel.sync(pool, post->get_key(), {1, 4, 5},
    [](auto ec, auto attached, auto detached) { });

// Toggle: attach if not present, detach if present
tags_rel.toggle(pool, post->get_key(), {1, 6},
    [](auto ec, auto attached, auto detached) { });

Parameters for belongs_to_many<Related>(pivot, local_fk, related_fk, related_table):

  • pivot — Pivot table name.
  • local_fk — Foreign key referencing this model in the pivot table.
  • related_fk — Foreign key referencing the related model in the pivot table.
  • related_table — Related model's table name (default: Related::table).

A morph_many relationship allows the related model to belong to multiple parent models using a single table. For example, comments can belong to both posts and videos.

cpp
// On Comment model:
auto commentable() {
    return morph_many<Comment>("commentable", "Post");
    //                          └── morph name  └── morph type value
}

// On Post model:
auto comments() {
    return morph_many<Comment>("commentable", "Post");
}

// Loading
post->load(pool, post->comments(), [](auto ec, auto comments) { });

The related table stores both the foreign key and the "type" column (e.g., commentable_id and commentable_type on the comments table). There is no pivot table — the type discriminator lives on the related model's table.

Parameters for morph_many<Related>(morph_name, morph_type, type_column, id_column, related_table):

  • morph_name — Morph name used for column naming ({name}_id, {name}_type).
  • morph_type — The value stored in the type column (e.g., "Post", "Video").
  • type_column — Override the type column name (default: "{morph_name}_type").
  • id_column — Override the ID column name (default: "{morph_name}_id").
  • related_table — Related table name (default: Related::table).

Lazy Loading

Load a relationship only if it hasn't been loaded yet. Useful for conditional access patterns.

cpp
user->load_missing(pool, user->posts(), [](auto ec, auto posts) { });

Accessing Loaded Relations

After loading, access the related models through type-safe accessors.

cpp
// Get a collection (has_many, belongs_to_many, morph_many)
auto posts = user->relation_vec<Post>("posts");

// Get a single item (has_one, belongs_to)
auto profile = user->relation_one<Profile>("profile");

The string parameter must match the relation name used in load() or load_missing().