Skip to content

Relations

Ferro provides a robust system for connecting models, supporting standard relational patterns with zero-boilerplate reverse lookups and automated join table management.

One-to-Many

The most common relationship type. It is defined using a ForeignKey on the "child" model and a BackRelationship marker on the "parent" model.

from typing import Annotated
from ferro import Model, ForeignKey, BackRelationship

class Author(Model):
    id: int
    name: str
    # Marker for reverse lookup; provides full Query intellisense
    posts: BackRelationship["Post"] = None

class Post(Model):
    id: int
    title: str
    # Defines the forward link and the name of the reverse field
    author: Annotated[Author, ForeignKey(related_name="posts")]

Shadow Fields

For every ForeignKey field (e.g., author), Ferro automatically creates a "shadow" ID column in the database (e.g., author_id). You can access or filter by this field directly via post.author_id.

One-to-One

A strict 1:1 link is created by adding unique=True to a ForeignKey.

class Profile(Model):
    user: Annotated[User, ForeignKey(related_name="profile", unique=True)]

Behavioral Difference:

  • Forward: Accessing await profile.user returns a single User object.
  • Reverse: Accessing await user.profile returns a single Profile object (internally calls .first()) instead of a Query object.

Many-to-Many

Defined using the ManyToManyField. Ferro automatically manages the hidden join table required for this relationship.

from ferro import ManyToManyField

class Student(Model):
    name: str
    courses: Annotated[list["Course"], ManyToManyField(related_name="students")] = None

class Course(Model):
    title: str
    students: BackRelationship["Student"] = None

Join Table Management

The Rust engine automatically registers and creates a join table (e.g., student_courses) when the models are initialized. You do not need to define a "through" model manually unless you need custom fields on the link.

Relationship Mutators

Many-to-Many relationships provide specialized methods for managing links:

  • .add(*instances): Create new links in the join table.
  • .remove(*instances): Remove specific links.
  • .clear(): Remove all links for the current instance.
await student.courses.add(math_101, physics_202)
await student.courses.clear()

Lazy Loading vs. Queries

Ferro relations are lazy. Data is never fetched until you explicitly request it.

  1. Forward Relations: Accessing a ForeignKey returns an awaitable descriptor.
    author = await post.author  # Database hit
    
  2. Reverse/M2M Relations: Accessing a BackRelationship or ManyToManyField returns a Query object. This allows you to chain further filters before execution.
    posts = await author.posts.where(Post.published == True).all()