Many-to-Many
Define many-to-many relationships connecting multiple records on both sides
Basic Example
import { s } from "viborm";
const post = s.model({
id: s.string().id().ulid(),
title: s.string(),
tags: s.manyToMany(() => tag),
});
const tag = s.model({
id: s.string().id().ulid(),
name: s.string().unique(),
posts: s.manyToMany(() => post),
});
VibORM automatically creates a junction table post_tag with postId and tagId columns.
Characteristics
| Aspect | Value |
|---|---|
| Returns | Array on both sides |
| Junction table | Auto-created or explicit |
| Can be empty | Yes (empty array) |
| FK location | Junction table |
Configuration
The distinctive rule of many-to-many: the FKs live in a junction table, configured with .through(), .A(), and .B(). See the relation method reference and referential actions:
s.manyToMany(() => tag)
.through("post_tags") // Junction table name
.A("post_id") // Source FK column in junction table
.B("tag_id") // Target FK column in junction table
.onDelete("cascade") // Referential action (applies to both FKs)
.onUpdate("cascade") // Referential action (applies to both FKs)
.name("labels") // Custom relation name
Auto-Generated Junction Table
Without explicit configuration, VibORM generates:
// For: post.tags = s.manyToMany(() => tag)
// Junction table: post_tag
// Columns: postId, tagId
Table name is derived from model names in alphabetical order: post + tag → post_tag.
Explicit Junction Table
For full control, note the A/B columns swap on the inverse side:
const post = s.model({
id: s.string().id().ulid(),
title: s.string(),
tags: s.manyToMany(() => tag)
.through("post_tags")
.A("post_id")
.B("tag_id"),
});
const tag = s.model({
id: s.string().id().ulid(),
name: s.string().unique(),
posts: s.manyToMany(() => post)
.through("post_tags")
.A("tag_id")
.B("post_id"),
});
Querying Many-to-Many
// Include tags when fetching post
const post = await client.post.findUnique({
where: { id: "post_123" },
include: { tags: true },
});
// post.tags: Tag[]
// Connect and disconnect tags
await client.post.update({
where: { id: "post_123" },
data: {
tags: {
connect: [{ id: "tag_1" }],
disconnect: [{ id: "tag_2" }],
}
}
});
See To-Many Relation Filters for some/every/none and Nested Writes for connect/disconnect/set/connectOrCreate.
Common Patterns
Explicit Junction Model
When you need additional data on the relation, model the junction table yourself with two many-to-one relations:
// Junction model with extra fields
const enrollment = s.model({
id: s.string().id().ulid(),
studentId: s.string(),
courseId: s.string(),
enrolledAt: s.dateTime().now(),
grade: s.string().nullable(),
student: s.manyToOne(() => student)
.fields("studentId")
.references("id"),
course: s.manyToOne(() => course)
.fields("courseId")
.references("id"),
})
.map("enrollments")
.unique(["studentId", "courseId"]);
const student = s.model({
id: s.string().id().ulid(),
name: s.string(),
enrollments: s.oneToMany(() => enrollment),
});
const course = s.model({
id: s.string().id().ulid(),
title: s.string(),
enrollments: s.oneToMany(() => enrollment),
});
Self-Referential Many-to-Many
const user = s.model({
id: s.string().id().ulid(),
name: s.string(),
following: s.manyToMany(() => user)
.through("user_follows")
.A("follower_id")
.B("following_id"),
followers: s.manyToMany(() => user)
.through("user_follows")
.A("following_id")
.B("follower_id"),
});