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(&mut conn) 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:
let mut conn = DbConnection::open(&config)?;let user = /* fetch user */;
let posts: Vec<Post> = user.posts.load(&mut conn)?;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, } }}
// Loadinglet profile: Option<Profile> = user.profile.load(&mut conn)?;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, } }}
// Loadinglet author: Option<User> = post.user.load(&mut conn)?;Under the hood: SELECT * FROM users WHERE id = ? LIMIT 1.
Complete User + Post example
use rust_web_server::model::{DbConfig, DbConnection, Value};use rust_web_server::model::relation::{HasMany, BelongsTo};
// --- Models ---
pub struct User { pub id: i64, pub name: String, pub posts: HasMany<Post>,}
pub struct Post { pub id: i64, pub user_id: i64, pub title: String, pub user: BelongsTo<User>,}
// --- Usage ---
let config = DbConfig::from_env()?;let mut conn = DbConnection::open(&config)?;
// Load a userlet rows = conn.query_raw("SELECT * FROM users WHERE id = ? LIMIT 1", &[Value::Int(1)])?;let row = rows.into_iter().next().unwrap();let user = User { id: row.get::<i64>("id")?, name: row.get::<String>("name")?, posts: HasMany::new(row.get::<Value>("id").unwrap_or(Value::Null), "user_id"),};
// Load that user's posts — one extra querylet posts: Vec<Post> = user.posts.load(&mut conn)?;
// Load the author of a post — one extra queryif let Some(post) = posts.first() { let author: Option<User> = post.user.load(&mut conn)?;}Avoiding N+1 queries
The explicit .load() design makes N+1 queries visible — if you call .load() 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::{DbConnection, ModelRow, Value};
// Step 1 — load users (1 query)let users: Vec<User> = conn.query::<User>("SELECT * FROM users WHERE active = ?", &[Value::Bool(true)])?;
// Step 2 — collect IDslet 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().enumerate() .map(|(i, _)| format!("?")) // use $N for PostgreSQL .collect::<Vec<_>>() .join(", "); let sql = format!("SELECT * FROM posts WHERE user_id IN ({})", placeholders); let posts: Vec<Post> = conn.query::<Post>(&sql, &user_ids)?;
// 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.