使用 MongoDB 引用

MongoDBBeginner
立即练习

介绍

在本实验中,你将学习如何使用 MongoDB 引用来建模数据之间的关系。你将构建一个简单的图书管理系统,包含 authorsbooks 集合。通过实践操作,你将学会创建文档、使用引用链接它们、跨集合查询相关数据、更新这些引用,并通过索引提高查询性能。本实验为你在 MongoDB 中进行数据建模提供了实践基础。

创建集合和引用文档

在此步骤中,你将设置数据库并创建两个集合:authorsbooks。你将通过将一本书与其作者关联来学习文档引用的基本概念。

首先,打开 MongoDB Shell。这个交互式 shell 是你运行所有数据库命令的地方。

mongosh

进入 shell 后,你将看到一个 test> 提示符。切换到一个名为 library_db 的新数据库。如果数据库不存在,MongoDB 会在你首次存储数据时创建它。

use library_db

现在,创建你的第一位作者。向 authors 集合插入一个文档。我们为这位作者指定了一个自定义的 _id,以便稍后方便引用。

db.authors.insertOne({
    _id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
    name: "Jane Austen",
    nationality: "British",
    birthYear: 1775
})

接下来,向 books 集合插入一个文档。author_id 字段包含你刚刚创建的作者的 ObjectId。这就是创建引用的方式。

db.books.insertOne({
    title: "Pride and Prejudice",
    author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
    published: 1813,
    genre: "Classic Literature"
})

你现在已经创建了一个一对一的关系。为了验证这一点,你可以检索刚刚创建的文档。

首先,查找作者:

db.authors.findOne({ name: "Jane Austen" })

示例输出:

{
  _id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
  name: 'Jane Austen',
  nationality: 'British',
  birthYear: 1775
}

现在,查找图书,并观察 author_id 字段,它链接到作者。

db.books.findOne({ title: "Pride and Prejudice" })

示例输出:

{
  _id: ObjectId("..."),
  title: 'Pride and Prejudice',
  author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
  published: 1813,
  genre: 'Classic Literature'
}

你可以在 mongosh shell 中继续进行下一步操作。

链接多个文档

一位作者通常会写不止一本书。在此步骤中,你将学习如何将多个“子”文档(图书)链接到一个“父”文档(作者)。这演示了一对多的关系。

继续在 mongosh shell 中操作。让我们再添加两本简·奥斯汀的书。使用 insertMany 命令一次插入多个文档。这两本新书将引用相同的 author_id

db.books.insertMany([
    {
        title: "Sense and Sensibility",
        author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
        published: 1811,
        genre: "Classic Literature"
    },
    {
        title: "Emma",
        author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
        published: 1815,
        genre: "Classic Literature"
    }
])

现在我们的数据库中有简·奥斯汀的三本书了,使用 find() 方法并按 author_id 过滤来检索所有这些书。

db.books.find({ author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1") })

示例输出:

[
  {
    _id: ObjectId("..."),
    title: 'Pride and Prejudice',
    author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
    published: 1813,
    genre: 'Classic Literature'
  },
  {
    _id: ObjectId("..."),
    title: 'Sense and Sensibility',
    author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
    published: 1811,
    genre: 'Classic Literature'
  },
  {
    _id: ObjectId("..."),
    title: 'Emma',
    author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
    published: 1815,
    genre: 'Classic Literature'
  }
]

你还可以使用 countDocuments 来快速统计与特定作者关联的书籍数量。

db.books.countDocuments({ author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1") })

示例输出:

3

这个简单的查询有效地确认了链接文档的数量。

使用 $lookup 跨集合查询

到目前为止,你已经通过使用已知的 author_id 来检索图书。一个更强大的方法是在单个查询中组合来自两个集合的数据。在此步骤中,你将使用 $lookup 聚合阶段,从 books 集合执行一个左外连接到 authors 集合。

首先,添加另一位作者和一本书,让我们的查询更有趣。

db.authors.insertOne({
    _id: ObjectId("6633c9a5b4e3e8a5c8a8f8b2"),
    name: "Charles Dickens",
    nationality: "British",
    birthYear: 1812
})
db.books.insertOne({
    title: "Oliver Twist",
    author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b2"),
    published: 1837,
    genre: "Historical Fiction"
})

现在,构建一个聚合管道。这个查询将从 books 集合开始,并为每本书“查找”匹配的作者。

db.books.aggregate([
    {
        $lookup: {
            from: "authors",
            localField: "author_id",
            foreignField: "_id",
            as: "author_details"
        }
    }
])

$lookup 阶段包含以下字段:

  • from: "authors": 指定要连接的集合。
  • localField: "author_id": 来自输入文档(来自 books)的字段。
  • foreignField: "_id": 来自“from”集合(来自 authors)文档的字段。
  • as: "author_details": 添加到输入文档的新数组字段的名称。

示例输出(针对一个文档):

{
  _id: ObjectId("..."),
  title: 'Pride and Prejudice',
  author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
  published: 1813,
  genre: 'Classic Literature',
  author_details: [
    {
      _id: ObjectId("6633c9a5b4e3e8a5c8a8f8b1"),
      name: 'Jane Austen',
      nationality: 'British',
      birthYear: 1775
    }
  ]
}

如你所见,作者的信息现在已嵌入到每个图书文档的 author_details 字段中。这允许你同时查询两个集合中的字段。

更新和维护引用

数据并非一成不变。你可能需要更正错误或删除数据,这需要更新或删除文档及其引用。在此步骤中,你将学习如何更新引用以及处理“孤儿”文档。

假设你发现图书“Emma”被错误地归属于简·奥斯汀,而实际上应该归属于查尔斯·狄更斯。你可以使用 updateOne 命令和 $set 操作符来更正此问题。

db.books.updateOne(
    { title: "Emma" },
    { $set: { author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b2") } }
)

通过再次查找该图书并检查其 author_id 来验证更改。

db.books.findOne({ title: "Emma" })

示例输出:

{
  _id: ObjectId("..."),
  title: 'Emma',
  author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b2"),
  published: 1815,
  genre: 'Classic Literature'
}

现在,让我们探讨删除父文档时会发生什么。如果我们删除一位作者,任何引用该作者的书籍都会变成“孤儿”。让我们从数据库中删除查尔斯·狄更斯。

db.authors.deleteOne({ name: "Charles Dickens" })

作者文档已不存在,但“Emma”和“Oliver Twist”这两本书仍然有一个指向已删除作者的 author_id。这可能导致数据完整性问题。在实际应用中,你将实现逻辑来处理这种情况,例如删除孤儿书籍或重新分配它们。

在本实验中,让我们通过删除这两本孤儿书籍来进行手动清理。

db.books.deleteMany({ author_id: ObjectId("6633c9a5b4e3e8a5c8a8f8b2") })

此命令会从 books 集合中删除所有引用已删除作者的文档,确保我们的数据保持一致。

通过索引提升查询性能

当你的集合不断增长时,按特定字段进行过滤的查询可能会变慢。这是因为 MongoDB 必须扫描每个文档来查找匹配项。为了优化这一点,你可以在经常查询的字段上创建索引。在我们的例子中,books 集合中的 author_id 是一个绝佳的候选字段。

在此步骤中,你将在 author_id 字段上创建一个索引,以加快查找作者图书的速度。

books 集合上使用 createIndex 命令。参数 { author_id: 1 } 告诉 MongoDB 在 author_id 字段上创建升序索引。

db.books.createIndex({ author_id: 1 })

MongoDB 将在后台处理此操作。完成后,它将返回一条消息,确认索引已创建。

示例输出:

{
  "numIndexesBefore": 1,
  "numIndexesAfter": 2,
  "createdCollectionAutomatically": false,
  "ok": 1
}

要验证索引是否存在,你可以使用 getIndexes 命令。这将列出 books 集合上的所有索引。

db.books.getIndexes()

你应该会看到两个索引:_id 上的默认索引以及你刚刚创建的新的 author_id_1 索引。

示例输出:

[
  { "v": 2, "key": { "_id": 1 }, "name": "_id_" },
  { "v": 2, "key": { "author_id": 1 }, "name": "author_id_1" }
]

有了这个索引,任何按 author_id 进行过滤或排序的查询,包括你之前使用的 $lookup 阶段,在大数据集上都会显著加快速度。

最后,你可以退出 MongoDB shell。

exit

总结

在本实验中,你学习了在 MongoDB 中使用文档引用的基础知识。你首先创建了集合,并使用 ObjectId 引用链接了文档。然后,你练习了管理一对多关系,使用强大的 $lookup 聚合阶段跨集合查询,并通过更新和清理引用来维护数据完整性。最后,你通过在引用字段上创建索引来提高了查询性能。这些技能对于使用 MongoDB 构建可扩展且高效的应用程序至关重要。