Skip to content

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)

rust
#[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)

rust
#[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.

rust
#[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.

rust
#[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

rust
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).

rust
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

rust
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)

rust
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?;

Built with ❤️ for the Rust Community.