嵌入式文档(Embedded Documents)和引用(References)是MongoDB中常用的两种数据建模方式,它们各有优缺点。选择使用哪种方式取决于具体的应用场景和需求。
嵌入式文档
优点
- 读取效率高:嵌入式文档通常可以在一次查询中获取所有相关数据,减少了数据库查询次数。
- 事务一致性:由于所有相关数据都在同一个文档中,更新时可以保证数据的一致性,不需要担心分布式事务问题。
- 简单性:数据模型简单,查询和处理方便。
缺点
- 文档大小限制:MongoDB文档大小限制为16MB,嵌入式文档如果过大,可能会超过这个限制。
- 冗余数据:在一些场景下,嵌入式文档会导致数据冗余,增加存储空间。
- 更新效率低:如果嵌入的数据结构复杂,更新可能会变得比较繁琐和低效。
引用
优点
- 数据去重:引用方式将数据分离到不同的集合中,减少了数据冗余。
- 灵活性高:各个集合独立更新,灵活性更高,适合复杂的数据模型。
- 文档大小分离:因为数据被分离到不同的文档中,不会受到单个文档大小的限制。
缺点
- 查询效率低:需要多次查询才能获取所有相关数据,特别是在大数据量下查询效率可能较低。
- 事务一致性差:跨集合的更新操作不能保证原子性,需要额外处理事务一致性问题。
- 复杂性高:数据模型复杂,需要处理数据的关联和联合查询。
使用示例
以下示例展示了在Node.js中如何使用嵌入式文档和引用,并对比它们的优缺点。
安装MongoDB的Node.js驱动
npm install mongodb
嵌入式文档示例
插入数据
const { MongoClient } = require('mongodb');
async function insertEmbeddedData() {
const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri, { useUnifiedTopology: true });
try {
await client.connect();
const db = client.db('myDatabase');
const usersCollection = db.collection('users');
await usersCollection.deleteMany({}); // 清空集合
await usersCollection.insertMany([
{
userId: 1,
name: "Alice",
address: {
street: "123 Main St",
city: "New York",
state: "NY",
zip: "10001"
},
orders: [
{ orderId: 101, amount: 50.5, date: new Date("2022-01-01") },
{ orderId: 102, amount: 100.0, date: new Date("2022-02-01") }
]
},
{
userId: 2,
name: "Bob",
address: {
street: "456 Maple Ave",
city: "San Francisco",
state: "CA",
zip: "94101"
},
orders: [
{ orderId: 103, amount: 75.0, date: new Date("2022-01-15") },
{ orderId: 104, amount: 200.0, date: new Date("2022-03-01") }
]
}
]);
console.log("Embedded data inserted");
} finally {
await client.close();
}
}
insertEmbeddedData().catch(console.error);
查询数据
async function queryEmbeddedData() {
const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri, { useUnifiedTopology: true });
try {
await client.connect();
const db = client.db('myDatabase');
const usersCollection = db.collection('users');
// 查询某个用户及其所有订单
console.log("\nQuery a user and their orders:");
let user = await usersCollection.findOne({ name: "Alice" });
console.log(user);
} finally {
await client.close();
}
}
queryEmbeddedData().catch(console.error);
嵌入式文档的查询效率很高,因为所有数据都在同一个文档中。
引用示例
插入数据
const { MongoClient, ObjectId } = require('mongodb');
async function insertReferencedData() {
const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri, { useUnifiedTopology: true });
try {
await client.connect();
const db = client.db('myDatabase');
const usersCollection = db.collection('users');
const ordersCollection = db.collection('orders');
await usersCollection.deleteMany({}); // 清空用户集合
await ordersCollection.deleteMany({}); // 清空订单集合
// 插入用户数据
const users = await usersCollection.insertMany([
{ name: "Alice" },
{ name: "Bob" }
]);
// 插入订单数据,并使用用户的ObjectId作为引用
await ordersCollection.insertMany([
{ userId: users.insertedIds[0], amount: 50.5, date: new Date("2022-01-01") },
{ userId: users.insertedIds[0], amount: 100.0, date: new Date("2022-02-01") },
{ userId: users.insertedIds[1], amount: 75.0, date: new Date("2022-01-15") },
{ userId: users.insertedIds[1], amount: 200.0, date: new Date("2022-03-01") }
]);
console.log("Referenced data inserted");
} finally {
await client.close();
}
}
insertReferencedData().catch(console.error);
查询数据
async function queryReferencedData() {
const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri, { useUnifiedTopology: true });
try {
await client.connect();
const db = client.db('myDatabase');
const usersCollection = db.collection('users');
const ordersCollection = db.collection('orders');
// 查询某个用户及其所有订单
console.log("\nQuery a user and their orders:");
let user = await usersCollection.findOne({ name: "Alice" });
let orders = await ordersCollection.find({ userId: user._id }).toArray();
console.log({ user, orders });
// 查询所有订单及其对应的用户信息
console.log("\nQuery all orders and their corresponding users:");
let results = await ordersCollection.aggregate([
{
$lookup: {
from: 'users', // 要关联的集合
localField: 'userId', // orders 集合中的字段
foreignField: '_id', // users 集合中的字段
as: 'user' // 输出数组字段
}
},
{
$unwind: '$user' // 展开数组
}
]).toArray();
console.log(results);
} finally {
await client.close();
}
}
queryReferencedData().catch(console.error);
通过这些示例,我们可以看到两种数据建模方式在操作上的不同:
-
嵌入式文档适用于小型数据集和简单的数据关系,读取效率高,数据一致性好,但是在数据量大或者关系复杂的情况下会导致文档过大,更新效率低。
-
引用适用于大型数据集和复杂的数据关系,数据去重和独立更新灵活,但查询效率低,特别是在需要多次查询获取相关数据时。此外,跨集合的事务一致性需要额外处理。
选择合适的数据建模方式取决于具体的应用场景和需求。对于一些应用,可以混合使用这两种方式,结合它们的优点,达到最佳的性能和灵活性。