One-to-many, many-to-many relationship modeling
Differences Between Relational Databases and NoSQL
Relational databases handle one-to-many and many-to-many relationships through foreign keys and junction tables. MongoDB, as a document database, models relationships using two approaches: nested documents and references. Embedded documents store related data within a single document, while references establish associations between documents using ObjectId
. The choice between these methods depends on query patterns and update frequency.
One-to-Many Relationship Modeling
Embedded Document Approach
Embedding is optimal when the number of child documents is limited and they are frequently queried together with the parent document. For example, articles and comments in a blog system:
// Article document
{
_id: ObjectId("5f8d8a7b2f4d4e1d2c3b4a5e"),
title: "MongoDB Relationship Modeling",
content: "...",
comments: [
{
author: "User A",
text: "Great article",
createdAt: ISODate("2023-05-01T10:00:00Z")
},
{
author: "User B",
text: "Very insightful",
createdAt: ISODate("2023-05-02T14:30:00Z")
}
]
}
The advantage of this structure is that complete data can be retrieved with a single query, but the downside is that the document size may grow continuously, potentially exceeding the 16MB limit.
Reference Approach
When the number of child documents is large or they need to be accessed independently, references are more suitable:
// Article document
{
_id: ObjectId("5f8d8a7b2f4d4e1d2c3b4a5e"),
title: "MongoDB Relationship Modeling",
content: "..."
}
// Comment document
{
_id: ObjectId("6a7b8c9d0e1f2a3b4c5d6e7f"),
articleId: ObjectId("5f8d8a7b2f4d4e1d2c3b4a5e"),
author: "User A",
text: "Great article",
createdAt: ISODate("2023-05-01T10:00:00Z")
}
Additional operations are required for queries:
// First query the article
const article = db.articles.findOne({_id: articleId});
// Then query related comments
const comments = db.comments.find({articleId: article._id}).toArray();
Many-to-Many Relationship Modeling
Bidirectional Reference Approach
Many-to-many relationships typically require an intermediate collection. For example, students and courses:
// Student document
{
_id: ObjectId("1a2b3c4d5e6f7a8b9c0d1e2f"),
name: "Zhang San",
enrolledCourses: [
ObjectId("a1b2c3d4e5f6a7b8c9d0e1f2"),
ObjectId("b2c3d4e5f6a7b8c9d0e1f2a3")
]
}
// Course document
{
_id: ObjectId("a1b2c3d4e5f6a7b8c9d0e1f2"),
name: "Database Principles",
students: [
ObjectId("1a2b3c4d5e6f7a8b9c0d1e2f"),
ObjectId("2b3c4d5e6f7a8b9c0d1e2f3a")
]
}
This approach requires maintaining consistency in bidirectional references, which can be ensured using transactions:
const session = db.getMongo().startSession();
session.startTransaction();
try {
// Student enrolls in a course
db.students.updateOne(
{ _id: studentId },
{ $addToSet: { enrolledCourses: courseId } },
{ session }
);
// Course adds a student
db.courses.updateOne(
{ _id: courseId },
{ $addToSet: { students: studentId } },
{ session }
);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
Intermediate Collection Approach
More complex many-to-many relationships can use a dedicated junction collection:
// Enrollment record
{
_id: ObjectId("c3d4e5f6a7b8c9d0e1f2a3b4"),
studentId: ObjectId("1a2b3c4d5e6f7a8b9c0d1e2f"),
courseId: ObjectId("a1b2c3d4e5f6a7b8c9d0e1f2"),
enrolledAt: ISODate("2023-09-01T09:00:00Z"),
grade: null
}
Query all courses a student is enrolled in:
const enrollments = db.enrollments.find({ studentId: studentId });
const courseIds = enrollments.map(e => e.courseId);
const courses = db.courses.find({ _id: { $in: courseIds } }).toArray();
Advanced Relationship Patterns
Tree Structure Modeling
Use parent references to store tree structures:
// Category document
{
_id: ObjectId("d4e5f6a7b8c9d0e1f2a3b4c5"),
name: "Electronics",
parentId: null
}
{
_id: ObjectId("e5f6a7b8c9d0e1f2a3b4c5d6"),
name: "Smartphones",
parentId: ObjectId("d4e5f6a7b8c9d0e1f2a3b4c5")
}
Query child categories:
const children = db.categories.find({ parentId: parentId }).toArray();
Graph Data Modeling
For graph data like social networks, a hybrid approach can be used:
// User document
{
_id: ObjectId("f6a7b8c9d0e1f2a3b4c5d6e7"),
name: "Li Si",
friends: [
ObjectId("a7b8c9d0e1f2a3b4c5d6e7f8"),
ObjectId("b8c9d0e1f2a3b4c5d6e7f8a9")
]
}
Query friends of friends (second-degree connections):
const user = db.users.findOne({_id: userId});
const friends = db.users.find({_id: {$in: user.friends}}).toArray();
const friendIds = friends.flatMap(f => f.friends);
const friendsOfFriends = db.users.find({
_id: {$in: friendIds, $ne: userId, $nin: user.friends}
}).toArray();
Query Optimization Techniques
Using $lookup for Join Operations
MongoDB 3.2+ supports the $lookup
operator for left-join-like operations:
db.orders.aggregate([
{
$lookup: {
from: "products",
localField: "productId",
foreignField: "_id",
as: "productDetails"
}
},
{
$unwind: "$productDetails"
}
]);
Application-Level Joins
For frequently accessed relationships, cache related data at the application level:
async function getArticleWithComments(articleId) {
const [article, comments] = await Promise.all([
db.articles.findOne({_id: articleId}),
db.comments.find({articleId}).toArray()
]);
return {
...article,
comments
};
}
Denormalization Design
For scenarios requiring high read performance, consider data redundancy:
// Order document
{
_id: ObjectId("a8b9c0d1e2f3a4b5c6d7e8f9"),
userId: ObjectId("1a2b3c4d5e6f7a8b9c0d1e2f"),
userName: "Wang Wu", // Redundant username to avoid joins
items: [
{
productId: ObjectId("b2c3d4e5f6a7b8c9d0e1f2a3"),
productName: "Wireless Earbuds",
price: 299
}
]
}
Schema Design Considerations
Read/Write Ratio Analysis
Data with high read frequency suits embedding, while data with high update frequency suits references. For example:
- Blog comments: read-heavy → embedded
- Stock transaction records: write-heavy → referenced
Data Consistency Requirements
Scenarios requiring strong consistency should use references with transactions:
// Fund transfer operation
const session = db.getMongo().startSession();
session.startTransaction();
try {
// Deduct from source account
db.accounts.updateOne(
{ _id: fromAccount, balance: { $gte: amount } },
{ $inc: { balance: -amount } },
{ session }
);
// Add to destination account
db.accounts.updateOne(
{ _id: toAccount },
{ $inc: { balance: amount } },
{ session }
);
// Record transaction
db.transactions.insertOne({
from: fromAccount,
to: toAccount,
amount,
timestamp: new Date()
}, { session });
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
Sharding Cluster Considerations
In sharded environments, related data should ideally reside on the same shard. For example, when sharding by user ID, order data should also include the user ID:
{
_id: ObjectId("c9d0e1f2a3b4c5d6e7f8a9b0"),
userId: ObjectId("1a2b3c4d5e6f7a8b9c0d1e2f"), // Shard key
orderDate: ISODate("2023-10-01"),
items: [...]
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:引用式关联