Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

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 + tagpost_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"),
});

Was this page helpful?