MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MongoDB入门指南:文档的基本概念

2021-06-111.2k 阅读

1. MongoDB 文档简介

在 MongoDB 中,文档(Document)是数据的基本单元,类似于关系型数据库中的行(Row),但它的结构更加灵活和强大。文档以键值对(key-value pairs)的形式存储数据,其中键是字符串,值可以是各种数据类型,包括基本数据类型(如字符串、数字、布尔值等)、复杂数据类型(如数组、嵌套文档等)。这种灵活性使得 MongoDB 能够轻松处理各种不同结构的数据,而无需像关系型数据库那样预先定义严格的表结构。

1.1 文档的表示形式

在 MongoDB 中,文档通常以 JSON(JavaScript Object Notation)格式表示。例如,以下是一个简单的 MongoDB 文档,用于表示一个人的信息:

{
    "name": "Alice",
    "age": 30,
    "email": "alice@example.com",
    "hobbies": ["reading", "painting"],
    "address": {
        "street": "123 Main St",
        "city": "Anytown",
        "state": "CA",
        "zip": "12345"
    }
}

在上述文档中,nameageemail 等是键,对应的值分别为字符串、数字、字符串。hobbies 键对应的值是一个数组,而 address 键对应的值是一个嵌套文档。

1.2 文档与集合的关系

文档存储在集合(Collection)中,集合类似于关系型数据库中的表(Table),但集合不需要预先定义结构。一个集合可以包含多个文档,这些文档可以具有不同的结构。例如,我们可以创建一个名为 users 的集合,然后向其中插入不同结构的用户文档:

// 使用 MongoDB Node.js 驱动
const { MongoClient } = require('mongodb');

async function insertDocuments() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const user1 = {
            "name": "Bob",
            "age": 25,
            "email": "bob@example.com"
        };

        const user2 = {
            "name": "Charlie",
            "email": "charlie@example.com",
            "phone": "123-456-7890"
        };

        const result1 = await usersCollection.insertOne(user1);
        const result2 = await usersCollection.insertOne(user2);

        console.log(`Inserted user1 with _id: ${result1.insertedId}`);
        console.log(`Inserted user2 with _id: ${result2.insertedId}`);
    } finally {
        await client.close();
    }
}

insertDocuments().catch(console.error);

在上述代码中,我们使用 MongoDB Node.js 驱动连接到本地 MongoDB 实例,然后在 test 数据库的 users 集合中插入了两个结构略有不同的用户文档。

2. 文档的数据类型

MongoDB 支持多种数据类型,这些数据类型使得文档能够存储丰富多样的数据。了解这些数据类型对于正确使用 MongoDB 至关重要。

2.1 基本数据类型

2.1.1 字符串(String)

字符串是最常见的数据类型之一,用于存储文本数据。在 MongoDB 中,字符串必须是 UTF - 8 编码的。例如:

{
    "name": "David",
    "department": "Engineering"
}

2.1.2 数字(Number)

MongoDB 支持多种数字类型,包括 32 位整数(Int32)和 64 位整数(Int64),以及 64 位浮点数(Double)。默认情况下,MongoDB 使用 Double 类型存储数字。例如:

{
    "age": 28,
    "salary": 5000.5
}

如果需要存储整数,可以显式指定类型。例如,在 MongoDB 的 shell 中:

db.users.insertOne({
    "age": NumberInt(28)
});

2.1.3 布尔值(Boolean)

布尔值用于表示真(true)或假(false)。例如:

{
    "isActive": true,
    "isAdmin": false
}

2.1.4 日期(Date)

日期类型用于存储日期和时间信息。在 MongoDB 中,日期以 UTC 时间存储。可以使用 JavaScript 的 Date 对象来创建日期值。例如:

const today = new Date();
db.orders.insertOne({
    "orderDate": today
});

2.1.5 null

null 类型表示一个空值或缺失值。例如:

{
    "middleName": null
}

2.2 复杂数据类型

2.2.1 数组(Array)

数组在 MongoDB 文档中非常有用,可以用来存储多个值或文档。例如,一个用户可能有多个电话号码:

{
    "name": "Eve",
    "phoneNumbers": ["123-456-7890", "098-765-4321"]
}

数组也可以包含嵌套文档。例如,一个订单可能包含多个订单项:

{
    "orderId": "12345",
    "orderItems": [
        {
            "product": "Laptop",
            "quantity": 1,
            "price": 1000
        },
        {
            "product": "Mouse",
            "quantity": 2,
            "price": 50
        }
    ]
}

2.2.2 嵌套文档(Embedded Document)

如前文提到的地址文档示例,嵌套文档允许在一个文档中嵌入另一个文档结构。这在表示具有复杂关系的数据时非常有用。例如,一个员工文档可以包含其经理的信息作为嵌套文档:

{
    "employeeName": "Frank",
    "manager": {
        "name": "Grace",
        "department": "Management"
    }
}

2.2.3 ObjectId

ObjectId 是 MongoDB 为每个文档自动生成的唯一标识符。它是一个 12 字节的十六进制字符串,由以下部分组成:

  1. 时间戳(4 字节):表示文档创建的时间。
  2. 机器标识符(3 字节):标识生成 ObjectId 的机器。
  3. 进程标识符(2 字节):标识生成 ObjectId 的进程。
  4. 计数器(3 字节):一个自增的计数器,用于确保在同一秒内生成的 ObjectId 是唯一的。

在插入文档时,如果没有指定 _id 字段,MongoDB 会自动为文档生成一个 ObjectId。例如:

const { MongoClient } = require('mongodb');

async function insertDocument() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const collection = database.collection('documents');

        const document = {
            "title": "Sample Document"
        };

        const result = await collection.insertOne(document);
        console.log(`Inserted document with _id: ${result.insertedId}`);
    } finally {
        await client.close();
    }
}

insertDocument().catch(console.error);

在上述代码中,我们插入了一个文档,MongoDB 为其自动生成了一个 ObjectId,并在控制台输出。

3. 文档的操作

对文档的操作是 MongoDB 开发的核心部分,包括插入、查询、更新和删除等操作。

3.1 插入文档

3.1.1 insertOne 方法

insertOne 方法用于向集合中插入单个文档。在 MongoDB 的 shell 中:

db.users.insertOne({
    "name": "George",
    "age": 35,
    "email": "george@example.com"
});

在 Node.js 中,使用 MongoDB 驱动:

const { MongoClient } = require('mongodb');

async function insertOneDocument() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const user = {
            "name": "George",
            "age": 35,
            "email": "george@example.com"
        };

        const result = await usersCollection.insertOne(user);
        console.log(`Inserted document with _id: ${result.insertedId}`);
    } finally {
        await client.close();
    }
}

insertOneDocument().catch(console.error);

3.1.2 insertMany 方法

insertMany 方法用于向集合中插入多个文档。在 MongoDB 的 shell 中:

db.users.insertMany([
    {
        "name": "Hank",
        "age": 22,
        "email": "hank@example.com"
    },
    {
        "name": "Ivy",
        "age": 27,
        "email": "ivy@example.com"
    }
]);

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function insertManyDocuments() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const users = [
            {
                "name": "Hank",
                "age": 22,
                "email": "hank@example.com"
            },
            {
                "name": "Ivy",
                "age": 27,
                "email": "ivy@example.com"
            }
        ];

        const result = await usersCollection.insertMany(users);
        console.log(`Inserted ${result.insertedCount} documents`);
        result.insertedIds.forEach((id, index) => {
            console.log(`Inserted document ${index + 1} with _id: ${id}`);
        });
    } finally {
        await client.close();
    }
}

insertManyDocuments().catch(console.error);

3.2 查询文档

3.2.1 find 方法基础

find 方法用于从集合中查询文档。最简单的形式是查询集合中的所有文档。在 MongoDB 的 shell 中:

db.users.find();

这将返回 users 集合中的所有文档。在 Node.js 中:

const { MongoClient } = require('mongodb');

async function findAllDocuments() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const cursor = usersCollection.find({});
        const users = await cursor.toArray();
        console.log(users);
    } finally {
        await client.close();
    }
}

findAllDocuments().catch(console.error);

3.2.2 条件查询

可以在 find 方法中传递查询条件来筛选文档。例如,查询年龄大于 30 岁的用户: 在 MongoDB 的 shell 中:

db.users.find({ "age": { "$gt": 30 } });

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function findUsersAboveAge() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const query = { "age": { "$gt": 30 } };
        const cursor = usersCollection.find(query);
        const users = await cursor.toArray();
        console.log(users);
    } finally {
        await client.close();
    }
}

findUsersAboveAge().catch(console.error);

3.2.3 复合条件查询

可以使用逻辑运算符(如 $and$or)进行复合条件查询。例如,查询年龄大于 30 岁且城市为 "New York" 的用户: 在 MongoDB 的 shell 中:

db.users.find({
    "$and": [
        { "age": { "$gt": 30 } },
        { "address.city": "New York" }
    ]
});

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function findUsersWithComplexCondition() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const query = {
            "$and": [
                { "age": { "$gt": 30 } },
                { "address.city": "New York" }
            ]
        };
        const cursor = usersCollection.find(query);
        const users = await cursor.toArray();
        console.log(users);
    } finally {
        await client.close();
    }
}

findUsersWithComplexCondition().catch(console.error);

3.3 更新文档

3.3.1 updateOne 方法

updateOne 方法用于更新集合中的单个文档。例如,将名为 "Alice" 的用户的年龄增加 1: 在 MongoDB 的 shell 中:

db.users.updateOne(
    { "name": "Alice" },
    { "$inc": { "age": 1 } }
);

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function updateOneUser() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const filter = { "name": "Alice" };
        const update = { "$inc": { "age": 1 } };
        const result = await usersCollection.updateOne(filter, update);
        console.log(`Matched ${result.matchedCount} documents`);
        console.log(`Modified ${result.modifiedCount} documents`);
    } finally {
        await client.close();
    }
}

updateOneUser().catch(console.error);

3.3.2 updateMany 方法

updateMany 方法用于更新集合中的多个文档。例如,将所有用户的年龄增加 1: 在 MongoDB 的 shell 中:

db.users.updateMany(
    {},
    { "$inc": { "age": 1 } }
);

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function updateManyUsers() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const filter = {};
        const update = { "$inc": { "age": 1 } };
        const result = await usersCollection.updateMany(filter, update);
        console.log(`Matched ${result.matchedCount} documents`);
        console.log(`Modified ${result.modifiedCount} documents`);
    } finally {
        await client.close();
    }
}

updateManyUsers().catch(console.error);

3.4 删除文档

3.4.1 deleteOne 方法

deleteOne 方法用于删除集合中的单个文档。例如,删除名为 "Bob" 的用户: 在 MongoDB 的 shell 中:

db.users.deleteOne({ "name": "Bob" });

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function deleteOneUser() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const filter = { "name": "Bob" };
        const result = await usersCollection.deleteOne(filter);
        console.log(`Deleted ${result.deletedCount} document`);
    } finally {
        await client.close();
    }
}

deleteOneUser().catch(console.error);

3.4.2 deleteMany 方法

deleteMany 方法用于删除集合中的多个文档。例如,删除所有年龄小于 20 岁的用户: 在 MongoDB 的 shell 中:

db.users.deleteMany({ "age": { "$lt": 20 } });

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function deleteManyUsers() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const filter = { "age": { "$lt": 20 } };
        const result = await usersCollection.deleteMany(filter);
        console.log(`Deleted ${result.deletedCount} documents`);
    } finally {
        await client.close();
    }
}

deleteManyUsers().catch(console.error);

4. 文档的索引

索引在 MongoDB 中对于提高查询性能至关重要。就像在一本书的目录一样,索引可以帮助 MongoDB 更快地定位到满足查询条件的文档。

4.1 创建索引

4.1.1 单字段索引

单字段索引是基于单个字段创建的索引。例如,为 users 集合的 email 字段创建索引: 在 MongoDB 的 shell 中:

db.users.createIndex({ "email": 1 });

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function createIndex() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const result = await usersCollection.createIndex({ "email": 1 });
        console.log(`Index created: ${result}`);
    } finally {
        await client.close();
    }
}

createIndex().catch(console.error);

4.1.2 复合索引

复合索引是基于多个字段创建的索引。例如,为 users 集合的 agename 字段创建复合索引: 在 MongoDB 的 shell 中:

db.users.createIndex({ "age": 1, "name": 1 });

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function createCompoundIndex() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const result = await usersCollection.createIndex({ "age": 1, "name": 1 });
        console.log(`Compound index created: ${result}`);
    } finally {
        await client.close();
    }
}

createCompoundIndex().catch(console.error);

4.2 使用索引

当查询条件与索引字段匹配时,MongoDB 会使用索引来加速查询。例如,在创建了 email 字段的索引后,查询特定 email 的用户会更快: 在 MongoDB 的 shell 中:

db.users.find({ "email": "alice@example.com" });

在 Node.js 中:

const { MongoClient } = require('mongodb');

async function findUserByEmail() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const usersCollection = database.collection('users');

        const query = { "email": "alice@example.com" };
        const cursor = usersCollection.find(query);
        const user = await cursor.toArray();
        console.log(user);
    } finally {
        await client.close();
    }
}

findUserByEmail().catch(console.error);

5. 文档的嵌套与关联

在 MongoDB 中,处理数据之间的关系有两种常见方式:文档嵌套和文档关联。

5.1 文档嵌套

文档嵌套是将相关的数据直接嵌入到一个文档中。例如,一个订单文档可以直接嵌入订单项的信息:

{
    "orderId": "67890",
    "customer": "John Doe",
    "orderItems": [
        {
            "product": "T - Shirt",
            "quantity": 2,
            "price": 20
        },
        {
            "product": "Jeans",
            "quantity": 1,
            "price": 50
        }
    ]
}

这种方式适用于数据之间紧密关联且子文档数量不会过多的情况。它的优点是查询时可以通过一次查询获取所有相关数据,缺点是如果子文档频繁更新,可能会导致文档大小超出限制。

5.2 文档关联

文档关联是通过引用其他文档的 _id 来建立关系。例如,一个订单文档可以引用客户文档的 _id

{
    "orderId": "13579",
    "customerId": ObjectId("5f4f4f4f4f4f4f4f4f4f4f4f"),
    "orderItems": [
        {
            "product": "Book",
            "quantity": 3,
            "price": 15
        }
    ]
}

然后可以通过 customerId 来查找对应的客户文档。在 MongoDB 的 shell 中:

const order = db.orders.findOne({ "orderId": "13579" });
const customer = db.customers.findOne({ "_id": order.customerId });

这种方式适用于数据之间关系较为松散,或者子文档数量可能较多的情况。它的优点是数据的更新和维护更灵活,缺点是查询相关数据时可能需要进行多次查询。

6. 文档的规范化与反规范化

在设计 MongoDB 数据模型时,需要考虑文档的规范化与反规范化。

6.1 规范化

规范化是将数据分解为多个相关的集合,以减少数据冗余。例如,在一个电子商务系统中,可以将客户信息、订单信息和产品信息分别存储在不同的集合中,并通过 _id 进行关联。这种方式有助于保持数据的一致性,但可能会增加查询的复杂性,因为需要进行多表连接(在 MongoDB 中通过多次查询实现)。

6.2 反规范化

反规范化是有意在文档中引入一定的数据冗余,以提高查询性能。例如,在订单文档中重复客户的部分信息(如姓名、地址),这样在查询订单时就不需要再查询客户集合。但反规范化可能会导致数据一致性问题,因为如果客户信息发生变化,需要同时更新多个相关文档。

在实际应用中,通常需要根据具体的业务需求和性能要求来平衡规范化和反规范化。如果查询性能是关键,且数据一致性要求不是特别严格,可以适当采用反规范化;如果数据一致性非常重要,且查询复杂度可以接受,则倾向于规范化。

通过深入理解 MongoDB 文档的基本概念、数据类型、操作、索引、嵌套与关联以及规范化与反规范化,开发者能够更有效地使用 MongoDB 来构建高性能、灵活的数据存储和管理系统。无论是处理简单的业务数据还是复杂的大数据场景,这些知识都是至关重要的。