2019 年 2 月

第 34 卷,第 2 期

[数据点]

使用用于 MongoDB 的 Cosmos DB API 探索 Azure Cosmos DB 的多模型功能

作者 Julie Lerman

Julie Lerman在上月的专栏 (msdn.com/magazine/mt848702) 以及之前几期专栏中,我介绍了 Azure Cosmos DB 的不同使用方式。Azure Cosmos DB 是支持各种 NoSQL API 的全球分布式多模型数据库服务。不过,到目前为止,我在所有文章中只使用过一种特定 API,即 SQL API。借助此 API,可使用 SQL 作为查询语言与数据进行交互。借助其他模型之一,可使用用于 MongoDB 的大部分工具和 API,与 Cosmos DB 数据库进行交互。此模型还受益于 BSON 文档格式,这是紧凑、高效的二进制序列化格式,可实现简洁的数据类型编码。不过,如何才能通过这些其他模型(Cassandra、Gremlin 和 Table)来访问单一数据库呢?这个问题的答案就在基础数据库引擎中,它基于原子记录序列 (ARS) 数据模型,从而能够本机支持许多不同的 API 和数据模型。每个 API 都与可用来与数据交互的热门 NoSQL 引擎和数据结构(如 JSON)保持线协议兼容。

请务必注意,多模型 API 暂不可交换。Cosmos DB 数据库包含在 Cosmos DB 帐户中。可以在 Azure 订阅中有多个 Cosmos DB 帐户,但在创建帐户时,请选择要对相应帐户中的数据库使用哪个 API。一旦选择,这是可用于相应数据库的唯一 API。在讨论此问题时,Azure 文档使用“当前”这样的字词,以设定下列预期:在某一时间点在这些方面有更多灵活性。

我一直想要尝试使用另一个 API。除了不久前对 Azure 表存储进行的一些探索(与 Azure Cosmos DB 中的表 API 保持一致)(请参阅我在 2010 年 7 月发表的文章 msdn.com/magazine/ff796231)以外,我还尚未使用过其他类型的数据库。MongoDB 是最常用 API 之一,所以我决定对此进行探索,而且这次初探很有趣。  我将在这里分享一些我学到的内容,但请注意,本文并不是“MongoDB 入门”文章。建议学习 Nuri Halperin 的 Pluralsight 课程 (bit.ly/2SI2Vxw),其中包含 MongoDB 初级和专家级内容。本文还收录了其他许多资源的链接。

MongoDB 支持的目标对象更多是已在使用 MongoDB 的开发人员和系统,因为 Azure 带来了很多优势。甚至还有介绍了如何将数据从现有 MongoDB 数据库迁移到 Azure Cosmos DB 的指南 (bit.ly/2FhzmPi)。

我能够意识到的第一个优势是,借助 MongoDB,可以在开发期间在笔记本电脑上使用本地数据库实例。虽然在 Windows 上使用 Cosmos DB 仿真程序 (bit.ly/2sHNsAn) 时 SQL 和 MongoDB API 有这种优势,但此仿真程序仅在 Windows 上运行。不过,可以在各种 OS(包括 macOS)上安装 MongoDB(我使用的是 Community Edition)。这样一来,可以在本地仿真由 Mongo DB API 驱动的 Cosmos DB 数据库的基本功能。我将先从本地入手,先熟悉 MongoDB,再切换到 Azure 数据库。

有关所有受支持平台的安装说明,可以查看官方 MongoDB 文档 (bit.ly/2S96ywj)。也可以从 hub.docker.com/_/mongo 中拉取 Docker 映像,用于在 macOS、Linux 或 Windows 上运行 MongoDB。

安装完成后,立即使用 mongod.exe 命令开始进程。除非另有规定,否则 MongoDB 要求你应创建目录 /data/db,并拥有访问权限。可以在默认位置创建它(例如,在 Windows 或 macOS 中“应用程序”、“库”和“用户”文件夹所在的根文件夹中创建 c:\data\db),也可以将位置指定为“mongod”命令的参数。因为我直接在自己的开发计算机上进行测试,所以我使用了“sudo mongod”,以确保服务拥有所需的权限。Community Edition 默认在本地主机上的端口 27017 处(即 127.0.0.1:27017)运行。

与 MongoDB 交互的方法有不少。尽管我对 C# API 最感兴趣,但我发现很有用的做法是,先从尽可能接近“基础”开始,再晋升到 API 之一。如果之前从未接触过 MongoDB,建议先使用(与服务一起安装的)shell 在命令行处执行某操作。只需键入“mongo”,即可启动 shell。 MongoDB 安装三种系统数据库:本地数据库、管理数据库和配置数据库。在 shell 中,键入“show dbs”可以列出数据库。

MongoDB 没有在 shell 或其他 API 中新建数据库的显式命令。相反,在你首次向使用的数据库插入数据时,如果数据库不存在,便会创建一个。使用以下命令尝试一下:

use myNewDatabase

如果随后直接调用“show dbs”,还不会看到 myNewDatabase。

shell 使用快捷方式“db”来处理当前数据库对象,即使实际数据库还不存在。

现在,需要插入文档。你将文档创建为 JSON,然后 API 使用二进制 BSON 格式将它们存储到 MongoDB 中。下面是示例文档:

{
  firstname : "Julie",
  lastname : "Lerman"
}

不过,文档不会直接存储在数据库中,它们需要存储在集合中。MongoDB 对集合和数据库的行为是相同的:如果你引用的集合尚不存在,它会在你向集合中插入数据时为你创建一个集合。因此,可以在插入新文档时引用新集合。请注意,一切都区分大小写。我将对新集合 MyCollection 使用 insertOne 方法来插入文档。另一个确认我的文档已插入的文档会返回,MongoDB 使用它自己的数据类型 ObjectId 为我已插入的文档提供唯一 ID 密钥:

> db.MyCollection.insertOne({firstname:"Julie",lastname:"Lerman"})
{
  "acknowledged" : true,
  "insertedId" : ObjectId("5c169d4f603846f26944937f")
}

现在,“show dbs”会列出 myNewDatabase,而且我也能使用 collection 对象的“find”命令来查询数据库了。我不会传递任何筛选参数或排序参数,因此它会返回所有文档(在我的示例中,只有一个文档):

>db.MyCollection.find()
{ "_id" : ObjectId("5c169d4f603846f26944937f"), "firstname" : "Julie", 
  "lastname" : "Lerman" }

我仅仅触及了最浅显的功能,现在是时候进一步深入了。通过在提示符处键入“exit”,退出 mongo shell。

使用 Visual Studio Code Cosmos DB 扩展

至此,已新建数据库,并在 shell 中执行了一些操作。现在,让我们来利用一些工具提供的功能。首先,我将使用 Visual Studio Code 中的 Azure Cosmos DB 扩展。如果尚无 VS Code,请访问 code.visualstudio.com 安装它。然后,可以在 VS Code 扩展窗格中安装此扩展。

若要在扩展中打开本地 MongoDB 实例,请右键单击“附加的数据库帐户”,并选择“附加数据库帐户”。 此时,系统会提示从多个 Cosmos DB API 中进行选择。选择“MongoDB”。下一个提示是要求提供数据库所在位置的地址。默认地址为 MongoDB 默认值,即 mongodb://127.0.0.1:27017。连接后,应该便能在 Cosmos DB 资源管理器中看到数据库,包括在 shell 中创建的数据库(其中含有集合和文档),如图 1 所示。


图 1:探索 MongoDB、服务器、数据库和数据

如果你编辑已打开的文档,此扩展会提示你将它更新到服务器。无论是连接到本地数据库,还是连接到云数据库,都会看到此提示。

.NET Core 应用中的 MongoDB

现在让我们再向上迈一个台阶,使用 .NET Core 应用中的 MongoDB 数据库。各种可用的驱动程序之一是 .NET 驱动程序 (bit.ly/2BvUEFl)。当前版本 2.7 与 .NET Framework 4.5 和 4.6 以及 .NET Core 1.0 和 2.0(包括次要版本)兼容。我使用的是最新 .NET Core SDK (2.2),并先使用 dotnet 命令行接口 (CLI) 命令在我命名为“Mongo­Test”的文件夹中新建控制台项目:

dotnet new console

接下来,我将使用 CLI 将对用于 MongoDB 的 .NET 驱动程序(名为 mongocsharpdriver)的引用添加到项目中:

dotnet add package mongocsharpdriver

我将创建简单的应用程序,它依赖的是上月专栏文章中的模型,内含与《苍穹浩瀚》书和电视剧相关的一些类。 我有两个类,分别是 Ship 和 Character:

public Ship()
{
  Crew=new List<Character>();
}
  public string Name { get; set; }
  public Guid Id { get; set; }
  public List<Character> Crew{ get; set;}
}
public class Character
{
  public string Name { get; set; }
  public string Bio {get;set;}
}

请注意,Ship 有名为 Id 的 Guid。按照约定,MongoDB 会将这与它的必需属性 _id 相关联。Character 没有 Id。我已决定建模。在与 Ship 数据进行交互的上下文中,我总是希望能够看到相应 Ship 上的 Character。将它们存储在一起可以简化检索过程。不过,可能要在其他位置保留 Character 的更多详细信息。可以嵌入仅包含对 Character ID 的引用的对象,例如:

public List<Guid> Crew{ get; set;}

不过,这意味着每次需要包含 Character 列表的 Ship 时,都不得不进行查找。混合替换方法是将 Id 属性添加到 Character,这样就能在需要时交叉引用了。作为子文档,Character 的 Id 只是一个随机属性。MongoDB 只要求根文档有 Id。但我已决定不要在初探时担心 Character 的 Id 问题。

建模时,需要做出很多决定。最重要的是,如果你默认思考的是关系数据库概念(需要在关系数据和对象之间进行大量转换),必须停止这样的思考,并考虑文档数据库模式和优点。我发现 Azure Cosmos DB 文档中关于如何对 NoSQL 数据库的文档数据进行建模的指南很有帮助:bit.ly/2kpF46A

对于 Id,还需要注意几点。如果你不向 Ship 的 Id 属性提供值,MongoDB 会为你创建值,就像 SQL Server 或其他数据库一样。如果根文档没有任何映射到已存储文档的 _id 的属性(按照约定或你自己的映射规则),它便无法尝试序列化包含 _id 的结果。

使用 mongocsharpdriver API

.NET 驱动程序的 API 始于 MongoClient 实例,从中可以与数据库以及集合和文档进行交互。此 API 反映了我已使用 shell 展示过的一些概念。例如,可以通过插入数据快速创建数据库和集合。下面的示例展示了如何使用这个新 API:

private static void InsertShip () {
  var mongoClient = new MongoClient ();
  var db = mongoClient.GetDatabase ("ExpanseDatabase");
  var coll = db.GetCollection<Ship> ("Ships");
  var ship = new Ship { Name = "Donnager" };
  ship.Characters.dd(new Character { Name = "Bobbie Draper", 
    Bio="Fierce Marine"});  
  coll.InsertOne (ship);
}

在 shell 中使用“use database”命令的位置,我现在对 MongoClient 对象调用 GetDatabase。数据库对象有泛型 GetCollection<T> 方法。我要将 Ship 指定为类型。字符串“Ships”是数据库中集合的名称。一旦定义,我就可以执行 InsertOne 或 InsertMany,就像在 shell 中一样。此外,.NET API 还提供了异步对应项,如 InsertOneAsync。

当我首次运行 InsertShip 方法时,新数据库和集合与新文档一起创建。如果我尚未插入新文档,而是仅引用了数据库和集合,它们不会快速创建。与 shell 一样,没有用于创建数据库的显式命令。

下面是在数据库中创建的文档:

{
  "_id": {
    "$binary": "TbKPi3+tLUK9b68lJkGaww==",
    "$type": "3"
  },
  "Name": "Donnager",
  "Characters": [
    {
      "Name": "Bobbie Draper",
      Bio: "Fierce Marine"
    }
  ]
}

但对我而言,更有趣的是类型化集合 (GetCollection<Ship>)。MongoDB 文档将集合描述为“类同于关系数据库中的表”(bit.ly/2QZcOcD),这是对文档数据库(在其中可以将不相关的随机文档存储到集合内)的有趣描述。不过,将集合绑定到特定类(与“ships”集合一样)的确表明我正在此集合中强制采用 ship 类型的架构。但这是针对 Collection 实例,而不是数据库中的实际集合。它会通知特定实例如何序列化和反序列化对象(假设可以将任何对象中的数据存储到一个集合中)。自版本 3.2 起,MongoDB 确实新增了强制采用架构验证规则的功能,尽管这不是默认设置。

我可以对其他类型使用相同的“Ships”集合:

var collChar = db.GetCollection<SomeOtherTypeWithAnId> ("Ships");

不过,这可能会在需要检索数据时造成问题。需要通过一种方法来标识集合中的文档类型。如果读过上月有关适用于 EF Core 的 Cosmos DB 提供程序(使用 SQL API)的文章,可能会记得,当 EF Core 将文档插入 Cosmos DB 后,它会添加 Discriminator 属性,这样就能始终确保文档与什么类型保持一致。可以对 MongoDB API 执行相同操作,但这可能会有点简单粗暴,因为 MongoDB 使用类型鉴别器来指定对象继承 (bit.ly/2sbHvgA)。我已添加继承自 Ship 的新类 DecommissionedShip:

public class DecomissionedShip : Ship {
  public DateTime Date { get; set; }
}

API 有名为 BsonClassMap 的类,用于指定自定义映射,包括它的 SetDiscriminatorIsRequired 方法。这会默认注入类名。因为将要替代默认映射,所以还需要添加 Automap 方法。

我向 program.cs 添加了新方法 ApplyMappings,并将它作为 Main 方法的第一行进行调用。这会专门指示 API 为 Ship 和 DecommissionedShip 添加鉴别器:

private static void ApplyMappings () {
    BsonClassMap.RegisterClassMap<Ship> (cm => {
      cm.AutoMap ();
      cm.SetDiscriminatorIsRequired (true);
    });
    BsonClassMap.RegisterClassMap<DecommissionedShip> (cm => {
      cm.AutoMap ();
      cm.SetDiscriminatorIsRequired (true);
    });
  }

我将 InsertShip 方法修改为,另外新建 DecommissionedShip。因为它继承自 Ship,所以我可以使用一个 Ship 集合实例及其 InsertMany 命令,将两个 ship 都添加到数据库中:

var decommissionedShip=new DecommissionedShip{Name="Canterbury", 
  Date=new DateTime(2350,1,1)};
coll.InsertMany(new[]{ship,decommissionedShip});

两个文档都被插入 Ships 集合,且每个文档都有添加为属性“_t”的鉴别器。 以下是 DecommissionedShip:

{
  "_id": {
    "$binary": "D1my7H9MrkmGzzJGSHOZfA==",
    "$type": "3"
  },
  "_t": "DecommissionedShip",
  "Name": "Canterbury",
  "Characters": [],
  "Date": {
    "$date": "2350-01-01T05:00:00.000Z"
  }
}

从类型化为 Ship 的集合中检索数据时,就像下面的 GetShip 方法中一样:

private static void GetShips ()
{
  var coll = db.GetCollection<Ship> ("Ships");
  var ships = coll.AsQueryable ().ToList ();
}

API 读取鉴别器,并具体化 Ship 和 DecommissionedShip 对象(它们的所有数据保持不变),包括分配到 DecommissionedShip 的 Date。

另一种映射路径是,使用不依赖特定类型的 BsonDocument 类型化集合对象。请查看我的博客文章“几种使用 MongoDB C# API 的编码模式”,了解如何使用 BsonDocuments,以及如何通过封装 MongoClient、Database 和 Collection 来提高代码的可读性。

使用 LINQ 进行查询

通过 API 方法或 LINQ,可使用 .NET API 检索文档。API 使用非常丰富的 Find 方法(类似于 shell 的查找方法),此方法返回游标。可以使用它的执行方法或聚合方法之一(其中很多方法类似于 LINQ 方法),传入筛选器、项目属性和返回对象。由于 .NET API Find 方法需要使用筛选器,因此若要获取任何文档,可以按新的(空的)BsonDocument 筛选,它是匹配任何文档的筛选器。对于 LINQ,首先需要将集合转换为 IQueryable(使用 AsQueryable()),再使用熟悉的 LINQ 方法来筛选、排序和执行。如果没有在类中添加或映射 _id 属性,需要使用投影逻辑将此考虑在内,因为它会从查询返回。可以参考文档或其他文章(如 Peter Mbanugo 撰写的精彩系列,网址为 bit.ly/2Lqqw2J),详细了解这些详情。

切换到 Azure Cosmos DB

在本地对 MongoDB 实例生成暂留逻辑后,你最终希望将它移到基于云的 Cosmos DB。借助 Visual Studio Code 的 Azure Cosmos DB 扩展,可以轻松新建 Cosmos DB 帐户(如果还没有的话),尽管很可能想要在门户中调整它的设置。如果你使用的是 Visual Studio,Cloud Explorer for VS2017 扩展提供浏览功能,但不提供数据库创建功能,因此在这种情况下,需要使用 Azure CLI 或门户。

下面介绍了如何使用 VS Code 扩展通过用于 MongoDB 的 Azure Cosmos DB API 从头开始新建实例。

首先,需要将 VS Code 连接到 Azure 订阅。这就需要使用 Azure 帐户扩展 (bit.ly/2k1phdp)。安装后,此扩展就有助于连接。连接后,Cosmos DB 扩展便会显示订阅和所有现有数据库。与图 1 中的 MongoDB 本地连接一样,可以钻取 Cosmos DB 帐户、数据库、集合和文档(用于表示这些的 Cosmos DB 术语是容器和项)。若要创建全新帐户,请右键单击扩展的资源管理器顶部的加号。工作流类似于我之前用来创建本地 MongoDB 数据库的工作流。系统会提示你输入帐户名。我将使用 datapointsmongodbs。接下来,从可用 API 中选择“MongoDB”,并新建 Azure 资源或选择现有资源来绑定到帐户。我新建了 Azure 资源,这样就能完全删除资源,并根据需要测试数据库。此后,必须从应托管此资源的数据中心区域中进行选择。我住在美国东部,所以我选择“美国东部”位置。考虑到 Cosmos DB 是全局数据库,可以在门户或其他应用中控制区域使用,但在我的演示不需要这样做。此时,帐户正在创建,需要等待几分钟。

创建后,新帐户便会出现在资源管理器中。右键单击此帐户,并选择“复制连接字符串”选项。使用此选项,可以将 MongoDB 驱动程序更改为指向 Azure Cosmos DB 实例,而不是指向默认本地实例,如下所示:

var connString=
  "mongodb://datapointsmongodbs:****.documents.azure.com:10255/?ssl=true";
ExpanseDb=new MongoClient(connString).GetDatabase("ExpanseDatabase");

我将运行方法的重构版本,以将新的 Ship 和 DecommissionedShip 插入 ExpanseDatabase 的 Ships 集合。当我在资源管理器中刷新数据库后,资源管理器便会显示我的 Azure 帐户中新创建的数据库,以及 Cosmos DB 数据库中的集合和文档,如图 2**** 所示。

新创建的数据库,以及 Cosmos DB 数据库中的集合和文档
图 2:新创建的数据库,以及 Cosmos DB 数据库中的集合和文档

虽不是 Mongo DB 专家,但能更好地了解多模型

此 API 可用并不是要说服像我这样使用 SQL 达几十年的用户转为对我的 Cosmos DB 数据库使用 MongoDB。我需要学习的地方还有很多。真正的目标是,让各种已使用 MongoDB 的开发人员和团队在受益于 Azure Cosmos DB 的同时熟悉使用它。我亲自探索了用于 MongoDB 的 Azure Cosmos DB API,以更好地了解 Cosmos DB 多模型功能,并让新数据库的探索过程变得有趣一点。但愿我的体验能够在将来为其他开发人员或客户提供一些简要指导。


Julie Lerman 住在佛蒙特州的丘陵地区,担任 Microsoft 区域主管、Microsoft MVP、软件团队导师和顾问。可以在全球的用户组和会议中看到她对数据访问和其他主题的介绍。她的博客地址是 thedatafarm.com/blog。她是“Entity Framework 编程”及其 Code First 和 DbContext 版本(全都出版自 O’Reilly Media)的作者。请关注她的 Twitter:@julielerman

衷心感谢以下技术专家对本文的审阅:Nuri Halperin (Plus N Consulting)


在 MSDN 杂志论坛讨论这篇文章