从开发一开始就考虑应用程序的数据需求非常重要。但是,如果您的应用程序将使用 NoSQL,并且您来自 RDBMS/SQL 背景,那么您可能会认为根据 NoSQL 来查看数据可能会很困难。本文将通过向您展示一些基本数据建模概念如何应用于 NoSQL 领域来帮助您。
我将使用 MongoDB 进行讨论,因为它是领先的开源 NoSQL 数据库之一,因为它的简单性、性能、可扩展性和活跃的用户群。当然,本文假设您了解基本的 MongoDB 概念(如集合和文档)。如果没有,我建议您阅读 SitePoint 上的一些以前的文章,以开始使用 MongoDB。
了解关系
关系显示您的 MongoDB 文档如何相互关联。要了解组织文档的不同方式,让我们看看可能的关系。
一对一关系 (1:1)
当实体的一个对象与另一个实体的一个且仅一个对象相关时,存在 1:1 关系。例如,一个用户可以有一个且只有一个出生日期。因此,如果我们有一个存储用户信息的文档和另一个存储出生日期的文档,它们之间将存在 1:1 的关系。
一对多关系 (1:N)
当一个实体的一个对象可以与另一个实体的许多对象相关时,就存在这种关系。例如,用户与其联系号码之间可能存在 1:N 关系,因为一个用户可能拥有多个号码。
多对多关系 (M:N)
当一个实体的一个对象与另一个实体的多个对象相关时,就会存在这种关系,反之亦然。如果我们将其与用户和他们购买的物品的情况相关联,一个用户可以购买不止一件物品,以及一件物品可以被多个用户购买。
建模一对一关系 (1:1)
考虑以下示例,我们需要为每个用户存储地址信息(现在假设每个用户都有一个地址)。在这种情况下,我们可以设计一个具有以下结构的嵌入文档:
{
"_id": ObjectId("5146bb52d8524270060001f4"),
"user_name": "Mark Benzamin"
"dob": "12 Jan 1991",
"address": {
"flat_name": "305, Sunrise Park",
"street": "Cold Pool Street",
"city": "Los Angeles"
}
}
我们将address
实体嵌入到用户实体中,使所有信息都存在于单个文档中。这意味着我们可以通过单个查询查找和检索所有内容。
// query to find user 'Mark Benzamin' and his address
$cursor = $collection->find(
array("user_name" => "Mark Benzamin"),
array("user_name" => 1,"address" => 1)
);
嵌入文档大致类似于反规范化,当两个实体之间存在“包含”关系时很有用。也就是说,一个文档可以存储在另一个文档中,从而将相关的信息片段放在单个文档中。由于所有信息都在一个文档中可用,因此这种方法具有更好的读取性能,因为文档内的查询操作对于服务器来说成本较低,并且我们可以在同一查询中查找和检索相关数据。
相比之下,规范化方法需要两个文档(最好是在单独的集合中),一个用于存储基本用户信息,另一个用于存储地址信息。第二个文档将包含一个user_id
字段,指示地址所属的用户。
{
"_id": ObjectId("5146bb52d8524270060001f4"),
"user_name": "Mark Benzamin",
"dob": "12 Jan 1991"
}
{
"_id": ObjectId("5146bb52d852427006000de4"),
"user_id": ObjectId("5146bb52d8524270060001f4"),
"flat_name": "305, Sunrise Park",
"street": "Cold Pool Street",
"city": "Los Angeles"
}
我们现在需要执行两个查询来获取相同的数据:
// query to find user information
$user = $collection->findOne(
array("user_name" => "Mark Benzamin"),
array("_id" => 1, "user_name" => 1)
);
// query to find address corresponding to user
$address = $collection->findOne(
array("user_id" => $user["_id"]),
array("flat_name" => 1, "street" => 1, "city" => 1)
);
第一个查询获取_id
用户的 ,然后在第二个查询中用于检索他的地址信息。
嵌入方法使得比在这种情况下,引用的方法,因为我们经常检索更有意义user_name
和address
在一起。您最终应该使用哪种方法取决于您如何在逻辑上连接实体以及您需要从数据库中检索哪些数据。
建模嵌入式一对多关系 (1:N)
现在让我们考虑一个用户可以拥有多个地址的情况。如果所有地址都应该与基本用户信息一起被检索,那么将地址实体嵌入用户实体中将是理想的。
{
"_id": ObjectId("5146bb52d8524270060001f4"),
"user_name": "Mark Benzamin"
"address": [
{
"flat_name": "305, Sunrise Park",
"street": "Cold Pool Street",
"city": "Los Angeles"
},
{
"flat_name": "703, Sunset Park",
"street": "Hot Fudge Street",
"city": "Chicago"
}
]
}
我们仍然能够通过单个查询获取所有必需的信息。引用/规范化方法会让我们设计三个文档(一个用户,两个地址)和两个查询来完成相同的任务。
除了效率和便利之外,我们应该在需要操作原子性的实例中使用嵌入式方法。由于任何更新都发生在同一个文档中,因此始终保证原子性。
建模引用的一对多关系 (1: N)
请记住,在应用程序的整个生命周期中,嵌入式文档的大小可能会继续增长,这可能会严重影响写入性能。每个文档的最大大小也有 16MB 的限制。如果嵌入的文档太大,嵌入方法会导致大量重复数据,或者如果您需要对文档之间的复杂或分层关系建模,则首选规范化方法。
考虑维护用户发布的帖子的示例。假设我们希望每个帖子都有用户的姓名和他的个人资料图片(类似于 Facebook 帖子,我们可以在每个帖子中看到姓名和个人资料图片)。非规范化方法会将用户信息存储在每个发布文档中:
{
"_id": ObjectId("5146bb52d8524270060001f7"),
"post_text": "This is my demo post 1",
"post_likes_count": 12,
"user": {
"user_name": "Mark Benzamin",
"profile_pic": "markbenzamin.jpg"
}
}
{
"_id": ObjectId("5146bb52d8524270060001f8"),
"post_text": "This is my demo post 2",
"post_likes_count": 32,
"user": {
"user_name": "Mark Benzamin",
"profile_pic": "markbenzamin.jpg"
}
}
我们可以看到这种方法在每个帖子文档中存储了冗余信息。展望未来,如果用户名或个人资料图片发生过更改,我们将不得不更新所有相应帖子中的相应字段。
因此,理想的方法是标准化信息并通过引用将其连接起来。
{
"_id": ObjectId("5146bb52d852427006000121"),
"user_name": "Mark Benzamin",
"profile_pic": "markbenzamin.jpg"
}
{
"_id": ObjectId("5146bb52d8524270060001f7"),
"post_text": "This is my demo post 1",
"post_likes_count": 12,
"user_id": ObjectId("5146bb52d852427006000121")
}
{
"_id": ObjectId("5146bb52d8524270060001f8"),
"post_text": "This is my demo post 2",
"post_likes_count": 32,
"user_id": ObjectId("5146bb52d852427006000121")
}
user_id
发布文档中的字段包含对用户文档的引用。因此,我们可以使用以下两个查询来获取用户发表的帖子:
$user = $collection->findOne(
array("user_name" => "Mark Benzamin"),
array("_id" => 1, "user_name" => 1, "profile_pic" => 1)
);
$posts = $collection->find(
array("user_id" => $user["_id"])
);
多对多关系建模 (M:N)
让我们以之前的示例为例,存储用户和他们购买的物品(最好在单独的集合中)并设计参考文档来说明 M:N 关系。假设存储用户信息文档的集合如下,每个文档都包含用户购买的商品列表的参考 ID。
{
"_id": "user1",
"items_purchased": {
"0": "item1",
"1": "item2"
}
}
{
"_id": "user2",
"items_purchased": {
"0": "item2",
"1": "item3"
}
}
类似地,假设另一个集合存储可用项目的文档。这些文档将依次存储购买它的用户列表的参考 ID。
{
"_id": "item1",
"purchased_by": {
"0": "user1"
}
}
{
"_id": "item2",
"purchased_by": {
"0": "user1",
"1": "user2"
}
}
{
"_id": "item3",
"purchased_by": {
"0": "user2"
}
}
要获取用户购买的所有商品,我们将编写以下查询:
// query to find items purchased by a user
$items = $collection->find(
array("_id" => "user1"),
array("items_purchased" => 1)
);
上述查询将返回 user1 购买的所有商品的 ID。我们稍后可以使用这些来获取相应的商品信息。
或者,如果我们想获取购买了特定商品的用户,我们将编写以下内容:
// query to find users who have purchased an item
$users = $collection->find(
array("_id" => "item1"),
array("purchased_by" => 1)
);
上述查询返回所有购买过 item1 的用户的 ID。我们稍后可以使用这些 ID 来获取相应的用户信息。
这个例子展示了在某些情况下非常有用的 M:N 关系。但是,您应该记住,很多时候可以使用 1:N 关系以及一些智能查询来处理此类关系。这减少了要在两个文档中维护的数据量。
结论
这就是本文。我们已经了解了一些基本的建模概念,它们肯定会帮助您开始自己的数据建模:1 对 1、1 对多和多对多关系,以及一些关于数据规范化和数据建模的知识。 -正常化。您应该能够轻松地将这些概念应用于您自己的应用程序的建模需求。如果您对文章有任何疑问或意见,请随时在下面的评论部分分享。