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

Preloading Associations

Preloading (also called eager loading) loads related records alongside the main query, avoiding extra database round-trips when you access associations.

Async means a query

Toasty’s API follows one rule for associations: if you .await it, it hits the database. The two ways to access an association make this visible in the code:

  • user.posts().exec(&mut db).await? — async, executes a query.
  • user.posts.get() — not async, reads already-loaded data in memory.

Because .get() is a plain (non-async) method, the compiler won’t let you confuse the two. You can scan any code path for .await to know exactly where database round-trips happen. This makes N+1 problems easy to spot and impossible to introduce by accident.

The N+1 problem

Without preloading, accessing a relation on each record in a list causes one query per record:

// 1 query: load all users
let users = User::all().exec(&mut db).await?;

for user in &users {
    // N queries: one per user to load their posts
    let posts = user.posts().exec(&mut db).await?;
    println!("{}: {} posts", user.name, posts.len());
}

If there are 100 users, this executes 101 queries. The .await on each user.posts().exec() call is a clear signal that a query runs on every iteration. Preloading reduces this to a fixed number of queries regardless of how many records you load.

Using .include()

Add .include() to a query to preload a relation. Pass the field path from the model’s fields() accessor:

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

// Access preloaded posts — .get() is not async, so no query happens
let posts: &[Post] = user.posts.get();
assert_eq!(1, posts.len());
Ok(())
}
}

The .include() call tells Toasty to load the associated posts as part of the query. After preloading, access the data through the field directly with user.posts.get() — a synchronous call that reads from memory. Compare this with user.posts().exec(&mut db).await?, which is async and always runs a query. The presence or absence of .await tells you whether code touches the database.

Preloaded vs unloaded access

There are two ways to access a relation, depending on whether it was preloaded:

Access patternAsyncWhen to useQueries
user.posts().exec(&mut db).await?YesRelation was not preloadedExecutes a query
user.posts.get()NoRelation was preloaded with .include()No query

Calling .get() on an unloaded relation panics. Only use .get() when you know the relation was preloaded.

Preloading BelongsTo

Preload a parent record from the child side:

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

// Access the preloaded user
let user: &User = post.user.get();
assert_eq!("Alice", user.name);
Ok(())
}
}

Preloading HasOne

Preload a single child record from the parent side:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    bio: String,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = toasty::create!(User { name: "Alice", profile: { bio: "A person" } })
    .exec(&mut db)
    .await?;
let user = User::filter_by_id(user.id)
    .include(User::fields().profile())
    .get(&mut db)
    .await?;

// Access the preloaded profile
let profile = user.profile.get().as_ref().unwrap();
assert_eq!("A person", profile.bio);
Ok(())
}
}

If no related record exists, the preloaded value is None rather than causing a panic:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    bio: String,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = toasty::create!(User { name: "No Profile" }).exec(&mut db).await?;

let user = User::filter_by_id(user.id)
    .include(User::fields().profile())
    .get(&mut db)
    .await?;

// Preloaded and empty — .get() returns None, not a panic
assert!(user.profile.get().is_none());
Ok(())
}
}

Multiple includes

Chain multiple .include() calls to preload several relations in one query:

let user = User::filter_by_id(user_id)
    .include(User::fields().posts())
    .include(User::fields().comments())
    .get(&mut db)
    .await?;

// Both are preloaded
let posts: &[Post] = user.posts.get();
let comments: &[Comment] = user.comments.get();

You can mix relation types — preload HasMany, HasOne, and BelongsTo relations in the same query:

let user = User::filter_by_id(user_id)
    .include(User::fields().profile())   // HasOne
    .include(User::fields().posts())     // HasMany
    .get(&mut db)
    .await?;

Preloading with collection queries

.include() works with .exec() (collection queries), not just .get() (single record). All records in the result have their relations preloaded:

let users = User::all()
    .include(User::fields().posts())
    .exec(&mut db)
    .await?;

for user in &users {
    // .get() is not async — no query per user, no N+1
    let posts: &[Post] = user.posts.get();
    println!("{}: {} posts", user.name, posts.len());
}

Summary

SyntaxDescription
.include(Model::fields().relation())Preload a relation in the query
model.relation.get()Access preloaded HasMany data (returns &[T])
model.relation.get()Access preloaded BelongsTo data (returns &T)
model.relation.get()Access preloaded HasOne data (returns &T or &Option<T>)
model.relation.is_unloaded()Check if a relation was preloaded