Mongoose provides various ways to handle relationships between documents, including References, Embedding, and Populate functionality.
Relationship Types
1. Embedding Relationships
Embed related data directly into the parent document, suitable for one-to-one or one-to-many relationships where child documents are small.
javascriptconst addressSchema = new Schema({ street: String, city: String, country: String }); const userSchema = new Schema({ name: String, address: addressSchema // Embedding relationship }); const User = mongoose.model('User', userSchema);
2. Reference Relationships
Reference other documents through ObjectId, suitable for one-to-many or many-to-many relationships.
javascriptconst authorSchema = new Schema({ name: String, email: String }); const bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' // Reference relationship } }); const Author = mongoose.model('Author', authorSchema); const Book = mongoose.model('Book', bookSchema);
Populate Functionality
Populate is a powerful feature in Mongoose that automatically replaces referenced ObjectIds with complete documents.
Basic Populate
javascript// Create author and book const author = await Author.create({ name: 'John Doe', email: 'john@example.com' }); const book = await Book.create({ title: 'My Book', author: author._id }); // Use populate to get complete author information const populatedBook = await Book.findById(book._id).populate('author'); console.log(populatedBook.author.name); // "John Doe"
Multiple Field Populate
javascriptconst bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' }, publisher: { type: Schema.Types.ObjectId, ref: 'Publisher' } }); const book = await Book.findById(id) .populate('author') .populate('publisher');
Nested Populate
javascriptconst commentSchema = new Schema({ text: String, user: { type: Schema.Types.ObjectId, ref: 'User' } }); const postSchema = new Schema({ title: String, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }] }); const post = await Post.findById(id) .populate({ path: 'comments', populate: { path: 'user' } });
Field Selection
javascriptconst book = await Book.findById(id) .populate({ path: 'author', select: 'name email' // Only select specific fields });
Conditional Populate
javascriptconst books = await Book.find() .populate({ path: 'author', match: { status: 'active' } // Only populate authors matching condition });
Many-to-Many Relationships
Using Array References
javascriptconst studentSchema = new Schema({ name: String, courses: [{ type: Schema.Types.ObjectId, ref: 'Course' }] }); const courseSchema = new Schema({ title: String, students: [{ type: Schema.Types.ObjectId, ref: 'Student' }] }); const Student = mongoose.model('Student', studentSchema); const Course = mongoose.model('Course', courseSchema); // Add course to student const student = await Student.findById(studentId); student.courses.push(courseId); await student.save(); // Query all courses for a student const studentWithCourses = await Student.findById(studentId).populate('courses');
Using Intermediate Collection
javascriptconst enrollmentSchema = new Schema({ student: { type: Schema.Types.ObjectId, ref: 'Student' }, course: { type: Schema.Types.ObjectId, ref: 'Course' }, enrolledAt: { type: Date, default: Date.now } }); const Enrollment = mongoose.model('Enrollment', enrollmentSchema); // Query all courses for a student const enrollments = await Enrollment.find({ student: studentId }) .populate('course');
Virtual Field Populate
Use virtual fields to create dynamic relationships:
javascriptconst authorSchema = new Schema({ name: String, books: [{ type: Schema.Types.ObjectId, ref: 'Book' }] }); const bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' } }); // Add virtual field to Book Schema bookSchema.virtual('authorBooks', { ref: 'Book', localField: 'author', foreignField: 'author' }); const Book = mongoose.model('Book', bookSchema); // Enable virtual field const book = await Book.findById(id).populate('authorBooks');
Performance Optimization
- Selective Populate: Only populate needed fields
- Limit Quantity: Use
limitto limit number of populated documents - Pagination: Use
skipandlimitfor pagination - Avoid N+1 Queries: Design data structure properly
- Use Indexes: Create indexes for reference fields
javascriptconst books = await Book.find() .populate({ path: 'author', select: 'name', options: { limit: 10 } });
Best Practices
- Choose embedding or reference based on data access patterns
- Avoid excessive nesting and deep populate
- Consider using virtual fields for complex relationships
- Create indexes for reference fields to improve query performance
- Consider using intermediate collections for many-to-many relationships
- Be aware of potential performance issues with populate