sqlmodel 0.5.0

SQL databases in Rust, designed to be intuitive and type-safe
# Chapter 4: Relationships & Loading

In relational databases, entities are connected by foreign keys. **SQLModel Rust** provides strongly-typed relationship wrappers that model one-to-one, one-to-many, and many-to-many associations while strictly preventing hidden **N+1 query** performance bugs.

---

## Declaring Relationships

Relationships are modeled using three specialized container types:
- `Related<T>`: Represents a to-one relationship loaded alongside the parent.
- `Lazy<T>`: Represents a to-one relationship that can be loaded on demand via a `Session`.
- `RelatedMany<T>`: Represents a to-many relationship holding a collection of related records.

```rust
use serde::{Deserialize, Serialize};
use sqlmodel::prelude::*;
use sqlmodel_core::{Lazy, RelatedMany};

#[derive(Model, Debug, Clone, Serialize, Deserialize)]
#[sqlmodel(table = "teams")]
pub struct Team {
    #[sqlmodel(primary_key, auto_increment)]
    pub id: Option<i64>,
    pub name: String,

    #[sqlmodel(relationship(model = "Player", foreign_key = "team_id", cascade_delete))]
    pub players: RelatedMany<Player>,
}

#[derive(Model, Debug, Clone, Serialize, Deserialize)]
#[sqlmodel(table = "players")]
pub struct Player {
    #[sqlmodel(primary_key, auto_increment)]
    pub id: Option<i64>,

    #[sqlmodel(foreign_key = "teams.id", on_delete = "CASCADE")]
    pub team_id: i64,

    pub name: String,

    #[sqlmodel(relationship(model = "Team", foreign_key = "team_id"))]
    pub team: Lazy<Team>,
}
```

---

## Eager Loading (`EagerLoader`)

When fetching a parent entity, related entities can be loaded in a single query via `EagerLoader`:

```rust,no_run
use sqlmodel::prelude::*;
use sqlmodel::EagerLoader;

# #[derive(Model, Debug, Clone, serde::Serialize, serde::Deserialize)]
# #[sqlmodel(table = "teams")]
# pub struct Team {
#     #[sqlmodel(primary_key)] pub id: Option<i64>,
#     pub name: String,
#     #[sqlmodel(relationship(model = "Player", foreign_key = "team_id"))]
#     pub players: sqlmodel_core::RelatedMany<Player>,
# }
# #[derive(Model, Debug, Clone, serde::Serialize, serde::Deserialize)]
# #[sqlmodel(table = "players")]
# pub struct Player {
#     #[sqlmodel(primary_key)] pub id: Option<i64>,
#     pub team_id: i64,
#     pub name: String,
# }
async fn fetch_teams_with_players(cx: &Cx, conn: &impl Connection) -> Outcome<Vec<Team>, Error> {
    let loader = EagerLoader::new().include("players");

    select!(Team)
        .eager(loader)
        .all_eager(cx, conn)
        .await
}
```

Under the hood:
1. SQLModel builds an optimized `LEFT JOIN` between `teams` and `players`.
2. Emitted columns carry unambiguous aliases (e.g. `players__name`, `players__team_id`).
3. The derive macro generates `Model::hydrate_relationship`, which deduplicates parents and groups child rows into `RelatedMany<Player>` collections automatically.

---

## Explicit Lazy Loading

If a related entity is not eagerly loaded upfront, you can load it on demand using the `Session`:

```rust,no_run
use sqlmodel::prelude::*;
use sqlmodel::Session;

# #[derive(Model, Debug, Clone, serde::Serialize, serde::Deserialize)]
# #[sqlmodel(table = "teams")]
# pub struct Team {
#     #[sqlmodel(primary_key)] pub id: Option<i64>,
#     pub name: String,
# }
# #[derive(Model, Debug, Clone, serde::Serialize, serde::Deserialize)]
# #[sqlmodel(table = "players")]
# pub struct Player {
#     #[sqlmodel(primary_key)] pub id: Option<i64>,
#     pub team_id: i64,
#     pub name: String,
#     #[sqlmodel(relationship(model = "Team", foreign_key = "team_id"))]
#     pub team: sqlmodel_core::Lazy<Team>,
# }
async fn inspect_player_team(
    cx: &Cx,
    session: &mut Session<impl Connection>,
    player: &Player,
) -> Outcome<bool, Error> {
    // Explicitly load the lazy relationship
    session.load_lazy(&player.team, cx).await
}
```

---

## Batch Loading to Eliminate N+1 Queries

When dealing with lists of models, sequentially calling `load_lazy` in a loop produces $O(N)$ database queries. SQLModel provides batch loaders to fetch relations for an entire slice in a single round-trip:

```rust,no_run
use sqlmodel::prelude::*;
use sqlmodel::Session;

# #[derive(Model, Debug, Clone, serde::Serialize, serde::Deserialize)]
# #[sqlmodel(table = "teams")]
# pub struct Team {
#     #[sqlmodel(primary_key)] pub id: Option<i64>,
#     pub name: String,
# }
# #[derive(Model, Debug, Clone, serde::Serialize, serde::Deserialize)]
# #[sqlmodel(table = "players")]
# pub struct Player {
#     #[sqlmodel(primary_key)] pub id: Option<i64>,
#     pub team_id: i64,
#     pub name: String,
#     #[sqlmodel(relationship(model = "Team", foreign_key = "team_id"))]
#     pub team: sqlmodel_core::Lazy<Team>,
# }
async fn load_all_teams(
    cx: &Cx,
    session: &mut Session<impl Connection>,
    players: &[Player],
) -> Outcome<usize, Error> {
    // Collects all unique team_ids and executes:
    // SELECT * FROM teams WHERE id IN (?, ?, ...)
    session.load_many(cx, players, |p| &p.team).await
}
```

---

## Opt-in N+1 Query Detection

To catch accidental loop loading during development, `Session` provides an active N+1 detector:

```rust,no_run
use sqlmodel::prelude::*;
use sqlmodel::Session;

fn configure_debugging(session: &mut Session<impl Connection>) {
    // Set threshold: log warning if same relationship query fires > 5 times
    session.enable_n1_detection(5);
}
```

You can query `session.n1_stats()` at the end of a request cycle to inspect relationship load frequency and identify opportunities for eager or batch loading.

---

## Differences from Python SQLModel

- **No Implicit I/O on Field Access**: Python SQLAlchemy intercepts `player.team` via property descriptors and fires synchronous database queries in the background. In Rust, field access never triggers hidden I/O; loading requires explicit calls to `load_lazy` or `all_eager`.
- **Type-Enforced Loading States**: A relationship's availability is visible in the type system (`Lazy<T>` vs loaded value), preventing uninitialized field panics.
- **Batch Loaders by Default**: While Python developers often reach for libraries like `strawberry` dataloaders to prevent N+1 issues, SQLModel Rust provides `Session::load_many` and `Session::load_one_to_many` out of the box.
- **Built-in N+1 Metrics**: An integrated query frequency detector alerts developers to repeated relationship loads before code reaches production.