2. Relationships
Rullst ORM generates powerful SQL relationships entirely through procedural macros. You can traverse and eager-load nested trees with high efficiency (O(1) queries to eliminate the N+1 problem).
Defining Relationships
Use the #[orm(...)] attribute to define relations on a field. Rullst requires you to wrap relationship collections in standard Rust data structures, like Option<T> for single models or Vec<T> for multiple.
Belongs To (1:1 / N:1)
#[derive(Debug, Clone, FromRow, Orm)]
pub struct Post {
pub id: i32,
pub title: String,
pub user_id: i32,
// This post belongs to the User matching `user_id` -> `id`
#[orm(belongs_to(model = "User", foreign_key = "user_id", local_key = "id"))]
pub user: Option<User>,
}Has Many (1:N)
#[derive(Debug, Clone, FromRow, Orm)]
pub struct User {
pub id: i32,
pub name: String,
// This user has many Posts where `posts.user_id` == `users.id`
#[orm(has_many(model = "Post", foreign_key = "user_id", local_key = "id"))]
pub posts: Vec<Post>,
}Morph Many (Polymorphic)
Useful when a model can belong to more than one other model on a single association.
#[derive(Debug, Clone, FromRow, Orm)]
pub struct Comment {
pub id: i32,
pub body: String,
pub commentable_id: i32,
pub commentable_type: String, // E.g., "Post" or "Video"
}
#[derive(Debug, Clone, FromRow, Orm)]
pub struct Post {
pub id: i32,
#[orm(morph_many(model = "Comment", name = "commentable", local_key = "id"))]
pub comments: Vec<Comment>,
}Cascading Soft Deletes
When using Soft Deletes (deleted_at), you can configure relationships to automatically soft-delete dependent children when the parent is soft-deleted.
#[derive(Debug, Clone, FromRow, Orm)]
pub struct User {
pub id: i32,
// If the User is soft-deleted, all their posts will be soft-deleted automatically!
#[orm(has_many(model = "Post", foreign_key = "user_id", local_key = "id", cascade_soft_delete = true))]
pub posts: Vec<Post>,
}Eager Loading (With)
Eager loading drastically improves performance by avoiding the N+1 query problem. Instead of querying the database for a relation every time you iterate over a loop, .with() groups it into a single SQL statement.
Basic Eager Loading
let posts = Post::query()
.with_user() // Automatically generated by the macro!
.get()
.await?;
for post in posts {
// Access safely without a DB hit
if let Some(author) = post.user {
println!("Post by: {}", author.name);
}
}Constrained Eager Loading
You can pass closures to modify the nested query before execution (e.g., fetching only approved comments for a post).
let users = User::query()
.with_posts_constrained(|q| q.where_eq("status", "published").order_by_desc("created_at"))
.get()
.await?;Complex Joins
If you prefer building a flattened table dynamically rather than tree-like eager loading, use the join methods.
Inner Joins
let results = Post::query()
.join("users", "users.id", "=", "posts.user_id")
.where_eq("users.is_active", true)
.select_raw("posts.*, users.email")
.get()
.await?;Constrained Joins (Multiple ON Clauses)
let advanced_join = Post::query()
.join_constrained("users", |join| {
join.on("posts.user_id", "=", "users.id")
.on_eq("users.status", "active") // Secure bound variable!
})
.get()
.await?;