Skip to content

Relations

Relations are typed on the model. ForeignKey<T> and OneToOne<T> store the primary key, optionally cache the loaded object, and serialize as the key. Validation and JSON Schema follow T::Pk.

#[derive(Model, Serialize, Deserialize, Schema, Debug, Clone)]
#[model(table = "todos")]
pub struct Todo {
#[field(primary_key, auto)]
pub id: i64,
pub title: String,
#[field(related_name = "todos")]
pub owner: ForeignKey<User>,
}
  • Column default: {field}_id.
  • on_delete default: cascade. set_null requires Option<ForeignKey<..>>.
  • Fetch: todo.fetch_owner(&db).await? → Arc<User> (or Option<Arc<User>> when the field is optional).
  • Reverse: user.todos(&db) is a QuerySet<Todo> because of related_name. The target must be in the same crate.
  • A OneToOne is unique. Reverse accessor is an async method returning Option<Profile>.

Join a related field in a filter or projection:

Todo::objects(&db)
.filter(Todo::owner.join(User::name).eq("ann"))
.all()
.await?;

Todo::owner.join(User::name) is a field of Todo typed String. Further hops compose (author.join(Author::team).join(Team::name)). Joins compile to LEFT JOIN with a stable alias (owner, owner__team).

Declared on the model, not as a struct field (there is no column and no serialized form):

#[model(
table = "posts",
many_to_many(tags(Tag, through_table = "post_tags", related_name = "posts"))
)]
pub struct Post { /* .. */ }
let tags = post.tags(&db);
tags.add([&tag], &db).await?;
tags.remove([&tag], &db).await?;
tags.set([&tag], &db).await?;
tags.clear(&db).await?;
let names = tags.queryset().all().await?;

Defaults: through_table = {table}_{name}, source_column = {snake(Model)}_id, target_column = {snake(Target)}_id. Set both columns explicitly for a self-referential relation. through = ThroughModel uses an explicit through model whose table is the join table; omit through_table then.

related_name adds a reverse queryset on the target (a correlated EXISTS over the join table).

let todos = Todo::objects(&db)
.select_related(Todo::owner.relation())
.all()
.await?;

select_related adds a JOIN and fills the cached object on each ForeignKey / OneToOne so later fetch_* does not hit the database.

prefetch_related issues a follow-up query and attaches related rows. Reverse foreign-key and many-to-many prefetch_related are not modelled on the struct (no slot to hold the children); use ManyToManyManager::queryset() or a filtered QuerySet. Multi-hop prefetch is also deferred — use select_related for those.

MongoDB rejects joins (Feature::Joins) with a capability error before any I/O.

Reverse accessors are inherent methods generated by the declaring model’s derive. When the target is in another crate, filter by key:

Book::objects(&db).filter(Book::author.eq(id)).all().await?;