rent
Learn by example

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.

On this page