Appearance
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
| Type | SQL Pattern | Example |
|---|---|---|
has_many | Parent has many children | A user has many posts |
has_one | Parent has one child | A user has one profile |
belongs_to | Child belongs to parent | A post belongs to a user |
belongs_to_many | Many-to-many via pivot table | A post has many tags |
morph_many | Polymorphic many-to-many | Comments 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";
};has_many<Related> — One-to-many
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).
has_one<Related> — One-to-one
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").
belongs_to_many<Related> — Many-to-many
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).
morph_many<Related> — Polymorphic
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().