Skip to content

Relations

The ORM provides three relationship helpers: HasMany<T>, HasOne<O>, and BelongsTo<O>. Each holds the information needed to load related records on demand. There is no lazy loading — you call .load(&pool).await explicitly when you want the data.

HasMany

HasMany<T> represents a one-to-many relationship from an owner to a collection of child records. The child table has a foreign key column that holds the owner’s primary key.

use rust_web_server::model::relation::HasMany;
use rust_web_server::model::Value;
pub struct User {
pub id: i64,
pub name: String,
// Declares the relationship — stores owner PK + FK column name
pub posts: HasMany<Post>,
}

Construct the helper with the owner’s primary key value and the foreign key column name on the child table:

impl User {
pub fn new(id: i64, name: String) -> Self {
User {
posts: HasMany::new(Value::Int(id), "user_id"),
id,
name,
}
}
}

Load the related records by calling .load(&pool).await:

let pool = DbPool::from_env().await?;
let user = /* fetch user */;
let posts: Vec<Post> = user.posts.load(&pool).await?;

Under the hood this issues: SELECT * FROM posts WHERE user_id = ?.

HasOne

HasOne<O> represents a one-to-one relationship where the child table holds the foreign key (the inverse of BelongsTo).

use rust_web_server::model::relation::HasOne;
use rust_web_server::model::Value;
pub struct User {
pub id: i64,
pub name: String,
pub profile: HasOne<Profile>,
}
impl User {
pub fn new(id: i64, name: String) -> Self {
User {
profile: HasOne::new(Value::Int(id), "user_id"),
id,
name,
}
}
}
// Loading
let profile: Option<Profile> = user.profile.load(&pool).await?;

Under the hood: SELECT * FROM profiles WHERE user_id = ? LIMIT 1.

BelongsTo

BelongsTo<O> is the inverse side — the child record holds a foreign key that points to the owner’s primary key.

use rust_web_server::model::relation::BelongsTo;
use rust_web_server::model::Value;
pub struct Post {
pub id: i64,
pub title: String,
pub user_id: i64,
pub user: BelongsTo<User>,
}
impl Post {
pub fn new(id: i64, title: String, user_id: i64) -> Self {
Post {
user: BelongsTo::new(Value::Int(user_id)),
id,
title,
user_id,
}
}
}
// Loading
let author: Option<User> = post.user.load(&pool).await?;

Under the hood: SELECT * FROM users WHERE id = ? LIMIT 1.

Complete User + Post example

use rust_web_server::model::{DbPool, Value};
use rust_web_server::model::relation::{HasMany, BelongsTo};
let pool = DbPool::from_env().await?;
// Load a user
let rows = pool.query_raw("SELECT * FROM users WHERE id = ? LIMIT 1", &[Value::Int(1)]).await?;
let row = rows.into_iter().next().unwrap();
let user = User {
id: row.get::<i64>("id")?,
name: row.get::<String>("name")?,
posts: HasMany::new(Value::Int(row.get::<i64>("id")?), "user_id"),
};
// Load that user's posts — one extra query
let posts: Vec<Post> = user.posts.load(&pool).await?;
// Load the author of a post — one extra query
if let Some(post) = posts.first() {
let author: Option<User> = post.user.load(&pool).await?;
}

Avoiding N+1 queries

The explicit .load().await design makes N+1 queries visible — if you call .load().await inside a loop you will issue one query per iteration. The recommended pattern is to batch-load related records using IN (…).

use rust_web_server::model::{DbPool, Value};
// Step 1 — load users (1 query)
let users: Vec<User> = pool.query::<User>("SELECT * FROM users WHERE active = ?", &[Value::Bool(true)]).await?;
// Step 2 — collect IDs
let user_ids: Vec<Value> = users.iter().map(|u| Value::Int(u.id)).collect();
// Step 3 — build IN clause and load all posts (1 query)
if !user_ids.is_empty() {
let placeholders = user_ids.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
let sql = format!("SELECT * FROM posts WHERE user_id IN ({})", placeholders);
let posts: Vec<Post> = pool.query::<Post>(&sql, &user_ids).await?;
// Step 4 — group in memory
use std::collections::HashMap;
let mut posts_by_user: HashMap<i64, Vec<Post>> = HashMap::new();
for post in posts {
posts_by_user.entry(post.user_id).or_default().push(post);
}
}

This loads N users and all their posts in exactly 2 queries regardless of how many users there are.