4. Many-to-many tags
Post.tags and Tag.posts share the post_tags join table. Rent generates
transactional nested writes, connection, removal, traversal, filtering, and
eager-loading methods from that relationship.
For a new schema, the declarations do not need join-table boilerplate:
model Post {
id BigInt @id
tags Tag[] @relation("PostTags")
}
model Tag {
id BigInt @id
posts Post[] @relation("PostTags")
}Rent derives post_tags, post_id, and tag_id. Existing databases can override those names explicitly, and a
relationship with its own fields should be modeled as a normal entity such as Membership.
let post = client
.create_post()
.id(11)
.title("Relationships without boilerplate")
.body("Connect and create related tags in one transaction.")
.published(true)
.connect_author(&ada)
.connect_tags_entities([&rust])
.create_tags([client.create_tag().id(2).name("databases")])
.save()
.await?;
let loaded = client
.post()
.id_eq(post.id)
.with_tags(|tags| tags)
.only()
.await?;
let database_post = client
.post()
.where_tags(|tags| tags.name_eq("databases"))
.only()
.await?;
let systems = client
.create_tag()
.id(3)
.name("systems")
.save()
.await?;
let updated = client
.update_post_one(&post)
.title("Typed relationship updates")
.set_tags([&systems])
.save_one()
.await?;The post, newly created tags, and join-table rows commit together. If any write
fails, Rent rolls the complete nested write back. Relationship changes on an
update share the same transaction as scalar and version-field changes. The
set_tags atomically makes the supplied entities the complete collection. sync_tags_ids does the same when you
only have IDs. connect_tags, connect_tags_entities, and disconnect_tags apply incremental changes instead. The
generated add_post_tags and remove_post_tags methods remain available for one direct connection change.
Preview tags on every post
Apply a child window when a feed only needs a few tags per post:
use generated::tag;
let posts = client
.post()
.with_tags(|tags| {
tags
.order_by(tag::fields::ID)
.offset(1)
.limit(3)
})
.all()
.await?;Each post independently skips its first tag and receives up to three more. Rent filters accessible tags, joins their links, and applies the per-post window in the database. Duplicate links do not consume preview slots. A tag shared by several posts can appear in each post's collection; posts with no remaining tags stay in the result with a loaded empty collection. Nested eager loads can be configured inside the tag query too.
Large parent-key sets are deduplicated and batched within the database's parameter budget. Batches execute sequentially on the same pool or transaction context. Use an appropriate transaction isolation level when you need one consistent snapshot across the whole read. A policy-global limit cannot be repeated separately for each batch; Rent reports an actionable error when that combination requires a smaller parent page.
Relationships with their own data
When the connection has its own data, model it as an entity and name the generated traversal in RSL:
model User {
id BigInt @id
groups_through Group[] @relation("MembershipGroups")
@rent.through(name: "memberships", model: "Membership")
}
model Membership {
id BigInt @id
user_id BigInt
group_id BigInt
role String
user User @relation(fields: [user_id], references: [id])
group Group @relation(fields: [group_id], references: [id])
}@rent.through keeps the join row as a normal generated model while also generating a direct
User -> Group traversal, query_groups_through(), that joins the memberships table for you. The second half of
the chapter creates a Membership containing a role, then performs that traversal.
The complete schema for this chapter is
crates/rent/examples/tutorial_03_many_to_many/schema.rsl.
Run cargo run -p rent --example tutorial_03_many_to_many. The same join-table workflow runs under nextest.