Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HasMany

A HasMany relationship connects a parent model to multiple child records. The parent declares a Vec<T> relation field, usually wrapped in Deferred<_> for lazy loading. The child stores a foreign key pointing back to the parent via BelongsTo.

Defining a HasMany relationship

Add a #[has_many] field on the parent model. Use Deferred<Vec<T>> for lazy loading, or Vec<T> for eager loading. The child model must have a corresponding #[belongs_to] field.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[has_many]
    posts: toasty::Deferred<Vec<Post>>,
}

#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,

    #[index]
    user_id: u64,

    #[belongs_to(key = user_id, references = id)]
    user: toasty::Deferred<User>,

    title: String,
}
}

The #[has_many] attribute does not add any columns to the parent’s table. The relationship is stored entirely in the child’s foreign key column (user_id).

With an eager field, Toasty loads the children whenever it loads the parent:

#[has_many]
posts: Vec<Post>,

This behaves like an implicit .include(User::fields().posts()) on every query that returns User.

Querying children

Call the relation method on a parent instance to get an accessor for its children:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::Deferred<Vec<Post>>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::Deferred<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = toasty::create!(User { name: "Alice" }).exec(&mut db).await?;
let posts: Vec<Post> = user.posts().exec(&mut db).await?;

for post in &posts {
    println!("{}", post.title);
}
Ok(())
}
}

The generated SQL is:

SELECT * FROM posts WHERE user_id = ?;

All queries through the relation accessor are automatically scoped to the parent. user.posts() only returns posts belonging to that user.

Creating through the relation

Create a child record through the parent’s relation accessor. Toasty automatically sets the foreign key:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::Deferred<Vec<Post>>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::Deferred<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = toasty::create!(User { name: "Alice" }).exec(&mut db).await?;
let post = toasty::create!(in user.posts() { title: "Hello World" })
    .exec(&mut db)
    .await?;

assert_eq!(post.user_id, user.id);
Ok(())
}
}

You don’t need to set user_id — Toasty fills it in from the parent.

Nested creation

Create a parent and its children in a single call using the singular form of the relation name on the create builder:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::Deferred<Vec<Post>>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::Deferred<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = toasty::create!(User {
    name: "Alice",
    posts: [{ title: "First post" }, { title: "Second post" }],
})
.exec(&mut db)
.await?;

let posts = user.posts().exec(&mut db).await?;
assert_eq!(2, posts.len());
Ok(())
}
}

Toasty creates the user first, then creates each post with the user’s ID as the foreign key.

Inserting and removing children

Use .insert() and .remove() to link and unlink existing records.

Inserting

Associate an existing child record with a parent:

let post = toasty::create!(Post { title: "Orphan post", user_id: 0 }).exec(&mut db).await?;

// Associate the post with a user
user.posts().insert(&mut db, &post).await?;

This updates the child’s foreign key to point to the parent. You can also insert multiple records at once:

user.posts().insert(&mut db, &[post1, post2, post3]).await?;

If the child is already associated with a different parent, .insert() moves it to the new parent.

Removing

Disassociate a child from a parent:

user.posts().remove(&mut db, &post).await?;

What happens to the child depends on whether the foreign key is required or optional:

Foreign key typeEffect of .remove()
Required (user_id: u64)Deletes the child record
Optional (user_id: Option<u64>)Sets the foreign key to NULL

When the foreign key is required, the child cannot exist without a parent, so Toasty deletes it. When the foreign key is optional, the child remains in the database with a null foreign key.

Scoped queries

The relation accessor supports scoped queries — filtering, updating, and deleting within the parent’s children.

Filtering with .filter()

// Find posts with a specific condition
let drafts = user
    .posts()
    .filter(Post::fields().published().eq(false))
    .exec(&mut db)
    .await?;

Looking up by ID within the scope

let post = user.posts().get_by_id(&mut db, &post_id).await?;

This only returns the post if it belongs to the user. If the post exists but belongs to a different user, this returns an error.

Updating through the scope

user.posts()
    .filter_by_id(post_id)
    .update()
    .title("New title")
    .exec(&mut db)
    .await?;

Deleting through the scope

user.posts()
    .filter_by_id(post_id)
    .delete()
    .exec(&mut db)
    .await?;

Filtering parents by children

You can filter parent records based on conditions on their children using .any() on the relation field:

// Find users who have at least one published post
let users = User::filter(
    User::fields()
        .posts()
        .any(Post::fields().published().eq(true))
)
.exec(&mut db)
.await?;

Multi-step relations (via)

A HasMany can reach its target through a path of existing relations instead of a single foreign key. Declare it with via and a dotted chain of relation fields, read left to right from this model. This expresses a relationship that exists only through a third model — a user has many comments, each comment belongs to an article, so a user has many commented articles:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[has_many]
    comments: toasty::Deferred<Vec<Comment>>,

    // User → comments → article
    #[has_many(via = comments.article)]
    commented_articles: toasty::Deferred<Vec<Article>>,
}

#[derive(Debug, toasty::Model)]
struct Comment {
    #[key]
    #[auto]
    id: u64,

    #[index]
    user_id: u64,

    #[belongs_to(key = user_id, references = id)]
    user: toasty::Deferred<User>,

    #[index]
    article_id: u64,

    #[belongs_to(key = article_id, references = id)]
    article: toasty::Deferred<Article>,
}

#[derive(Debug, toasty::Model)]
struct Article {
    #[key]
    #[auto]
    id: u64,

    title: String,

    #[has_many]
    comments: toasty::Deferred<Vec<Comment>>,
}
}

The target type is Article because the path comments.article ends there. A via relation owns no foreign key — it is derived from the relations it traverses — so it needs no pair. Each step may be any relation kind, and the kinds can be mixed along one path.

Query a via relation like any other relation:

// Every article this user has commented on, each listed once.
let articles = user.commented_articles().exec(&mut db).await?;

// Filtered and ordered like any other relation query.
let recent = user
    .commented_articles()
    .filter(Article::fields().title().eq("Rust"))
    .exec(&mut db)
    .await?;

A via relation yields distinct targets: a target reached through several intermediates appears once. A user who comments on the same article twice gets that article once.

A via relation is read-only. You can query, filter, order, and preload it, but Toasty generates no create, insert, or remove methods — writing through a multi-step path would have to materialize intermediate records. Mutate the underlying relations directly instead.

Preload a via relation with .include() to avoid the N+1 — see Preloading Associations. Preloading or projecting a via relation is supported on SQL backends; both are not yet available on DynamoDB.

What gets generated

For a User model with #[has_many] posts: Deferred<Vec<Post>>, Toasty generates:

On the parent instance:

MethodReturnsDescription
user.posts()Relation accessorAccessor scoped to this user’s posts
.exec(&mut db)Result<Vec<Post>>All posts belonging to this user
.create()Create builderCreate a post with the foreign key pre-filled
.get_by_id(&mut db, &id)Result<Post>Get a post by ID within the scope
.filter(expr)Query builderFilter posts within the scope
.insert(&mut db, &post)Result<()>Associate an existing post with the user
.remove(&mut db, &post)Result<()>Disassociate a post from the user

On the create builder:

MethodDescription
toasty::create!(User { posts: [{ ... }] })Add children to create alongside the parent

On the fields accessor:

MethodDescription
User::fields().posts()Field path for preloading and filtering
User::fields().posts().any(expr)Filter parents by child conditions