Skip to main content

Overview

The manyToMany relationship represents a many-to-many (N:N) association where records from one model can relate to multiple records from another model, and vice versa. For example, Posts and Tags - a post can have many tags, and a tag can belong to many posts. This relationship requires a pivot table (junction table) to store the associations.

Method Signature

manyToMany(RelatedClass, pivotTable, parentKey = null, relatedKey = null)
The related Model class to connect with this many-to-many relationship
pivotTable
string
required
The name of the pivot (junction) table that stores the relationship mappings.This table should contain foreign keys referencing both models.
parentKey
string
default:"{CurrentModel._table}_id"
The foreign key column in the pivot table that references the current model.If not specified, defaults to the current model’s table name + _id (e.g., post_id for Post model).
The foreign key column in the pivot table that references the related model.If not specified, defaults to the related model’s table name + _id (e.g., tag_id for Tag model).

Basic Usage

Define the Relationship

class Post extends Model {
  static _table = "Posts";

  tags() {
    return this.manyToMany(Tag, "PostTag", "post_id", "tag_id");
  }
}

class Tag extends Model {
  static _table = "Tags";

  posts() {
    return this.manyToMany(Post, "PostTag", "tag_id", "post_id");
  }
}

Query with Eager Loading

Use with() to load the relationship efficiently:
// Load posts with their tags
const posts = Post.with("tags").get();

posts.each(post => {
  Logger.log(`${post.title} - Tags:`);
  
  // tags is a Collection
  post.tags.each(tag => {
    Logger.log(`  - ${tag.name}`);
  });
});

Return Value

When eager loaded with with(), the relationship property returns a Collection instance containing all related models.
manyToMany always returns a Collection, even if there are no related records (empty collection).
const post = Post.with("tags").first();

// post.tags is a Collection
Logger.log(`Tag count: ${post.tags.count()}`);

post.tags.each(tag => {
  Logger.log(tag.name);
});

Pivot Table Structure

The pivot table stores the relationships between the two models:
Posts table:
┌────┬─────────────────┐
│ id │ title           │
├────┼─────────────────┤
│ 1  │ First Post      │
│ 2  │ Second Post     │
└────┴─────────────────┘

Tags table:
┌────┬─────────────┐
│ id │ name        │
├────┼─────────────┤
│ 1  │ JavaScript  │
│ 2  │ Tutorial    │
│ 3  │ Advanced    │
└────┴─────────────┘

PostTag table (pivot):
┌─────────┬────────┐
│ post_id │ tag_id │
├─────────┼────────┤
│ 1       │ 1      │  Post 1 has Tag 1 (JavaScript)
│ 1       │ 2      │  Post 1 has Tag 2 (Tutorial)
│ 2       │ 1      │  Post 2 has Tag 1 (JavaScript)
│ 2       │ 3      │  Post 2 has Tag 3 (Advanced)
└─────────┴────────┘
  • Post 1 has tags: JavaScript, Tutorial
  • Post 2 has tags: JavaScript, Advanced
  • JavaScript tag appears in: Post 1, Post 2

Examples

Posts and Tags

class Post extends Model {
  static _table = "Posts";
  
  tags() {
    return this.manyToMany(Tag, "PostTag", "post_id", "tag_id");
  }
}

class Tag extends Model {
  static _table = "Tags";
  
  posts() {
    return this.manyToMany(Post, "PostTag", "tag_id", "post_id");
  }
}

// Get all posts with their tags
const posts = Post.with("tags").get();

posts.each(post => {
  const tagNames = post.tags.pluck("name").all().join(", ");
  Logger.log(`${post.title}: ${tagNames}`);
});

// Get all tags with their posts
const tags = Tag.with("posts").get();

tags.each(tag => {
  Logger.log(`Tag: ${tag.name}`);
  Logger.log(`Used in ${tag.posts.count()} posts`);
});

Students and Courses

class Student extends Model {
  static _table = "Students";
  
  courses() {
    return this.manyToMany(Course, "Enrollments", "student_id", "course_id");
  }
}

class Course extends Model {
  static _table = "Courses";
  
  students() {
    return this.manyToMany(Student, "Enrollments", "course_id", "student_id");
  }
}

// Get student's enrolled courses
const students = Student.with("courses").get();

students.each(student => {
  Logger.log(`${student.name} is enrolled in:`);
  
  student.courses.each(course => {
    Logger.log(`  - ${course.title}`);
  });
});

// Get course enrollment count
const courses = Course.with("students").get();

courses.each(course => {
  Logger.log(`${course.title}: ${course.students.count()} students`);
});

Products and Categories

class Product extends Model {
  static _table = "Products";
  
  categories() {
    return this.manyToMany(Category, "ProductCategory", "product_id", "category_id");
  }
}

class Category extends Model {
  static _table = "Categories";
  
  products() {
    return this.manyToMany(Product, "ProductCategory", "category_id", "product_id");
  }
}

// Find products in multiple categories
const products = Product.with("categories").get();

products.each(product => {
  const categoryNames = product.categories.pluck("name").all();
  Logger.log(`${product.name}: ${categoryNames.join(", ")}`);
});

Working with Collections

Since manyToMany returns a Collection, you have access to powerful methods:
const post = Post.with("tags").first();

// Count related records
const tagCount = post.tags.count();

// Get specific fields
const tagNames = post.tags.pluck("name").all();

// Filter related records
const popularTags = post.tags.where("popularity", ">", 100);

// Sort related records
const sortedTags = post.tags.sortBy("name");

// Check if collection has items
if (post.tags.isEmpty()) {
  Logger.log("Post has no tags");
}

// Convert to array
const tagsArray = post.tags.all();
const tagsJSON = post.tags.toJSON();

Nested Relationships

Load relationships through many-to-many connections:
class Post extends Model {
  static _table = "Posts";
  
  tags() {
    return this.manyToMany(Tag, "PostTag", "post_id", "tag_id");
  }
  
  author() {
    return this.belongsTo(User, "user_id");
  }
}

// Load posts with tags and authors
const posts = Post.with("tags", "author").get();

posts.each(post => {
  Logger.log(`${post.title} by ${post.author.name}`);
  Logger.log(`Tags: ${post.tags.pluck("name").all().join(", ")}`);
});

Default Keys Convention

If you follow naming conventions, you can omit the key parameters:
// Explicit keys (verbose)
class Post extends Model {
  static _table = "Posts";
  
  tags() {
    return this.manyToMany(Tag, "PostTag", "post_id", "tag_id");
  }
}

// Using defaults (assuming PostTag has post_id and tag_id columns)
class Post extends Model {
  static _table = "Posts";
  
  tags() {
    return this.manyToMany(Tag, "PostTag");
  }
}

Bidirectional Relationships

Define the relationship from both sides for full flexibility:
class Post extends Model {
  static _table = "Posts";
  
  tags() {
    return this.manyToMany(Tag, "PostTag", "post_id", "tag_id");
  }
}

class Tag extends Model {
  static _table = "Tags";
  
  posts() {
    // Note: parentKey and relatedKey are swapped
    return this.manyToMany(Post, "PostTag", "tag_id", "post_id");
  }
}

// Query from either side
const post = Post.with("tags").first();
Logger.log(post.tags.pluck("name"));

const tag = Tag.with("posts").first();
Logger.log(tag.posts.pluck("title"));

Performance Tips

Always use eager loading with with() to load the pivot table and related records efficiently.

Good: Eager Loading (3 queries)

// ✅ Efficient: 3 queries (Posts + Pivot + Tags)
const posts = Post.with("tags").get();

posts.each(post => {
  post.tags.each(tag => {
    Logger.log(tag.name);  // No additional queries
  });
});

Bad: N+1 Problem (Many queries)

// ❌ Inefficient: Multiple queries for each post
const posts = Post.all();

posts.each(post => {
  // Manual pivot lookup - very slow!
  const pivotRows = /* query PostTag where post_id = post.id */;
  pivotRows.forEach(pivot => {
    const tag = Tag.find(pivot.tag_id);  // Additional query per tag!
    Logger.log(tag.name);
  });
});

Common Use Cases

E-commerce: Products and Orders

class Order extends Model {
  products() {
    return this.manyToMany(Product, "OrderProduct", "order_id", "product_id");
  }
}

class Product extends Model {
  orders() {
    return this.manyToMany(Order, "OrderProduct", "product_id", "order_id");
  }
}

Social Network: Users and Roles

class User extends Model {
  roles() {
    return this.manyToMany(Role, "UserRole", "user_id", "role_id");
  }
}

class Role extends Model {
  users() {
    return this.manyToMany(User, "UserRole", "role_id", "user_id");
  }
}

Content Management: Articles and Authors

class Article extends Model {
  authors() {
    return this.manyToMany(Author, "ArticleAuthor", "article_id", "author_id");
  }
}

class Author extends Model {
  articles() {
    return this.manyToMany(Article, "ArticleAuthor", "author_id", "article_id");
  }
}
  • BelongsTo - Define many-to-one relationships
  • HasMany - Define one-to-many relationships
  • HasOne - Define one-to-one relationships
  • Collection - Work with collections of models

Build docs developers (and LLMs) love