3. User, Post, and Comment
The canonical tutorial expands into a blog: a user authors posts, and users comment on posts.
The schema keeps the routine declaration compact:
model Post {
id BigInt @id
author_id BigInt
author User @relation(fields: [author_id], references: [id])
comments Comment[]
}author User tells Rent this is required and to-one. User? declares a nullable relation, and User[] declares a
collection. RSL keeps the foreign-key fields visible and uses the same aligned array syntax for compound keys.
let post = client
.user()
.id_eq(ada.id)
.query_posts()
.only()
.await?;
let author = client
.post()
.id_eq(post.id)
.query_author()
.only()
.await?;Traversal methods change the query target. Eager-loading keeps the original target and fills its relations field:
let discussion = client
.post()
.id_eq(post.id)
.with_author(|query| query)
.with_comments(|query| query.with_author(|author| author))
.only()
.await?;
let author = discussion.author()?;
let comments = discussion.comments()?;Auto chooses the safe query shape by default. You can make the tradeoff explicit:
use rent::runtime::RelationLoadStrategy;
let post_with_author = client
.post()
.id_eq(post.id)
.relation_load_strategy(RelationLoadStrategy::Join)
.with_author(|author| author)
.only()
.await?;
let discussion = client
.post()
.id_eq(post.id)
.relation_load_strategy(RelationLoadStrategy::SelectIn)
.with_author(|author| author)
.with_comments(|comments| {
comments.with_author(|author| author)
})
.only()
.await?;The first query can load its direct to-one relationship in one statement. The second uses bounded batched queries for the nested relationship tree and avoids one query per post or comment.
For a feed, load a short comment preview for each post:
use generated::{comment, post};
let feed = client
.post()
.order_by(post::fields::ID)
.with_comments(|comments| {
comments
.order_by_desc(comment::fields::ID)
.limit(3)
})
.all()
.await?;The child limit applies independently to every post. Rent applies relationship windows in the database,
so a post with thousands of comments returns only its three selected comments. Posts without comments remain
in the feed with an empty, loaded collection. Add offset(3) inside the child query to request the next preview
window; limit(0) loads an empty collection. Generated primary-key fields break ordering ties deterministically.
Large direct and many-to-many relation key lists are deduplicated and split to fit the database's parameter budget. Batches run sequentially on the query's pool or transaction context. Use a transaction with suitable isolation when the entire read needs one consistent snapshot. A policy-imposed global limit is not a per-parent limit: if it cannot safely fit in one batch, Rent reports an error asking for a smaller parent page.
Relationship state is explicit. A plain query does not pretend that an unloaded collection is empty:
let post = client
.post()
.id_eq(post_id)
.only()
.await?;
assert!(!post.comments_is_loaded());
assert!(post.comments().is_err());After with_comments(...), comments_is_loaded() is true and comments()? returns the loaded slice, even when that slice is empty.
Connect a loaded entity without copying its ID through application code:
let post = client
.create_post()
.title("Notes on the engine")
.body("Typed relationship references")
.published(false)
.connect_author(&ada)
.save()
.await?;Use connect_author_id(ada.id) when the application only has an ID.
The same chapter adds a unique User.profile relationship, showing that cardinality changes the generated result from Vec<T> to Option<Box<T>>.
Create a related row inline when both records belong to one operation:
let post = client
.create_post()
.title("Notes on the engine")
.body("Created with its author atomically.")
.published(false)
.create_author(
client
.create_user()
.name("Charles"),
)
.save()
.await?;Rent opens a transaction only because this builder contains a nested create. It inserts the author, assigns the returned ID to author_id, and inserts the post. If either insert fails, both are rolled back. The executable chapter deliberately forces the post insert to fail and confirms that its nested author does not survive.
The focused relationship schema is
crates/rent/examples/tutorial_02_relationships/schema.rsl.
Run cargo run -p rent --example tutorial_02_relationships. The same relationship workflow runs under nextest.