rent
Concepts

Relationships

Rent supports one-to-one, one-to-many, many-to-many, recursive, inverse, and explicit join-entity relationships.

Declare the common case

The RSL type and field name provide the usual relationship metadata:

model Post {
  id       BigInt @id
  authorId BigInt
  author   User   @relation(fields: [authorId], references: [id])
  tags     Tag[]  @relation("PostTags")
}

model User {
  id    BigInt @id
  posts Post[]
}

model Tag {
  id    BigInt @id
  posts Post[] @relation("PostTags")
}

Rent infers a required to-one relationship from User and binds authorId to the target primary key. User? makes the relationship nullable. Tag[] establishes a collection and receives deterministic many-to-many storage when no foreign-key side exists.

let users = client
    .user()
    .with_posts(|posts| posts.with_comments(|comments| comments))
    .all()
    .await?;

let author = client
    .post()
    .id_eq(post_id)
    .query_author()
    .only()
    .await?;

Generated entity values expose eager-loaded data directly through user.posts()? and post.author()?. The accessors return RelationNotLoaded when the query did not request that relationship. Relationships compile to ordinary SQL foreign keys and join tables.

Referenced keys can have any valid field name and can map to different SQL column names with @map. For an alternate unique key, specify it explicitly in references, such as references: [email] for a unique User.email. The same connection, traversal, eager-loading, and nested-create methods apply. Many-to-many helpers use the declared primary keys on both sides. See the named-key example.

Migrations use the same key metadata: foreign keys reference the mapped SQL columns, and join columns use their parent key's storage type. For a compound identity, declare all fields and references explicitly. Use an explicit join model when either side of a many-to-many relationship has a compound identity.

Choose an eager-loading strategy

Auto is the default. It uses one joined statement for a simple to-one load and batched IN queries for collections, nested loads, and shapes where separate queries preserve pagination or ordering more safely.

use rent::runtime::RelationLoadStrategy;

let post = client
    .post()
    .id_eq(post_id)
    .relation_load_strategy(RelationLoadStrategy::Join)
    .with_author(|author| author)
    .only()
    .await?;

let users = client
    .user()
    .relation_load_strategy(RelationLoadStrategy::SelectIn)
    .with_posts(|posts| {
        posts.with_comments(|comments| comments)
    })
    .all()
    .await?;

Choose Join when one round trip matters for a direct relationship. Choose SelectIn when loading collections or a nested tree. If a requested join shape cannot preserve the query's semantics, Rent safely uses select-in loading.

Child limit and offset apply independently to each parent, including many-to-many collections. Rent bounds these results in SQL and batches large parent-key lists within the database's parameter budget. Many-to-many loads join target rows to their links in the child query; they do not fetch every link into the application first. The tag tutorial demonstrates per-post previews and shared tags.

Compound foreign keys use the same with_posts, query_author, and connect_author methods. Rent matches the entire key tuple, including when multiple tenants reuse the same user ID. Child windows partition by all relationship columns, and bind budgets account for every component. See compound identities and relationships for the model and executable example.

Create both sides atomically by nesting the related create builder:

let post = client
    .create_post()
    .title("Typed relationships")
    .create_author(
        client
            .create_user()
            .name("Ada"),
    )
    .save()
    .await?;

If either insert fails, Rent rolls back both rows. A create without nested writes remains a single insert.

Connect a foreign-key relationship through the generated relationship name. Rent assigns the backing key without making an extra database round trip:

let post = client
    .create_post()
    .title("Typed relationships")
    .connect_author(&user)
    .save()
    .await?;

let post = client
    .update_post_one(&post)
    .disconnect_editor()
    .save_one()
    .await?;

For a relationship that references the target's id, use connect_author_id(user_id) when only an ID is available. A relationship that references any other key, whether one alternate unique field or a compound key, provides connect_author_key(...) instead, taking the referenced values in the declared foreign-key field order. Connecting an entity whose referenced unique key contains null returns an error; use disconnect_author() to clear an optional relationship explicitly. For many-to-many collections, set_tags([&rust, &databases]) replaces the complete collection atomically; sync_tags_ids([rust_id, databases_id]) is its ID-only form. Incremental connect_tags, connect_tags_entities, and disconnect_tags methods are also generated.

The inferred join table is enough for a new schema:

tags Tag[] @relation("PostTags")

Many-to-many storage can also use application-specific SQL names:

tags Tag[] @relation("PostTags") @rent.join(table: "post_labels", columns: [post_id, label_id], constraints: [post_labels_post_fkey, post_labels_label_fkey])

Compound foreign keys use aligned local and referenced fields:

author User @relation(fields: [tenantId, authorId], references: [tenantId, id], onDelete: Cascade)

On this page