2019 年 1 月

第 34 卷,第 1 期

[数据点]

EF Core Cosmos DB 提供程序预览版速览

作者:Julie Lerman | 2019 年 1 月

EF Core Cosmos DB 提供程序处于预览状态。所有信息可能都会发生变更。

Julie Lerman本专栏的忠实读者应该知道我使用 Entity Framework (EF) 和较新的 EF Core 执行了大量操作,而且我也是 Azure Cosmos DB 的超级粉丝。乍一看,你可能认为对象关系映射器 (ORM) 和文档数据库(如 Cosmos DB)彼此之间没什么关系。ORM 旨在解决将对象映射到关系数据库的问题。使用文档数据库或其他类型的 NoSQL 数据库,则可以只将对象和对象的图形存储为 JSON,而不必担心关系数据库的约束。那么为什么我、EF 团队或其他开发人员会将这两者组合在一起呢?

为什么将 ORM 与 NoSQL 数据库结合在一起使用?

当 EF Core 最早的几个 beta 版本以 EF7 的形式出现时,它们包括一个概念证明提供程序,用于与 Azure 表存储进行交互,Azure 表存储是当时 Azure 上唯一的 NoSQL 数据库。在试用完之后,我意识到 EF7 提供了一个重要优势,现在 EF Core 和 Azure Cosmos DB 也同样提供这一优势。用于与数据库交互的客户端(例如 .NET 客户端或 Node.js SDK)需要大量的设置代码来标识数据库、容器和查询对象。(请注意,即将推出的客户端版本将降低这种复杂性。) 但是使用 EF Core,我注意到的第一件事就是我不必编写任何额外的代码。我只提供了一个连接字符串,就可以正常运行了。实际上,虽然认识到这个数据存储不是关系型,但在执行查询或保存数据等例行公事般的任务时,我就不必专注于此。我将介绍使用 EF Core 与数据存储交互的所有体验,并将它们指向不同的数据存储。提供程序负责提供查询的解释以及存储到数据库中的 JSON 文档的创建和读取。

虽然提供程序也可以使用约定创建数据库和容器,但你仍可以为数据库创建 Cosmos DB 帐户并确定其配置,以及调整每个数据库以提高性能并降低成本。这些是使用任意关系数据库执行的任务,因此将 EF Core 指向 Azure Cosmos DB 数据库时也没有什么不同。

EF Core 的新 Cosmos DB 提供程序作为 EF Core 2.2 的预览版发布。预计会在 EF Core 3.0 中完全发布。但是自从我在两年多前测试了 Azure 表存储的概念证明以来,我一直对如何使用它感到很好奇,所以我决定不等到它完全发布,我要提前一探究竟。而且我相信你们中的许多人也对此感到好奇。

Cosmos DB 客户端工具

Azure 门户有一个很棒的数据资源管理器,用于查看 Cosmos DB 数据库、容器和文档,但有时你不希望在 IDE 和网站之间来回切换。Visual Studio Code 和 Visual Studio 2017 都具有性能出色的扩展,可用于查看和编辑 Cosmos DB 数据库中的文档。VS Code 具有 Azure Cosmos DB 扩展 (bit.ly/2SuTXmS),Visual Studio 具有适于 Visual Studio 2017 的 Cloud Explorer 扩展 (bit.ly/2G3SNxj)。通过 Azure Cosmos DB 扩展,还可以使用预先存在的 Cosmos DB 帐户来动态创建和删除数据库和容器。

将提供程序引入解决方案

提供程序的工作方式与任何其他 EF Core 提供程序一样。在项目中引用其包,然后在 OnConfiguring 中指定它,或者,如果使用的是 ASP.NET Core,则在 Startup 中定义 DbContext 时进行引用。

将提供程序命名为 Microsoft.EntityFrameworkCore.Cosmos。(如果你好好回忆一下,就会发现这个名称已经比早期版本缩短了。) 和任何包一样,可以使用多种方法将它添加到项目中。在 Visual Studio 2017 中,可以使用程序包管理器。在命令行中,可以通过以下命令添加它:

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

或者,如果只想打开项目文件,则可以使用以下 PackageReference 将其添加到 ItemGroup 部分:

<ItemGroup>
  <PackageReference Include="Microsoft.EntityFrameworkCore.Cosmos"/>
</ItemGroup>

添加该包后,需要告诉 DbContext 使用此提供程序。我有一个名为 ExpanseContext 的 DbContext,它将我喜欢的电视节目/系列书籍的简单模型“The Expanse”映射到我的数据存储 - 一个 Azure Cosmos DB 数据库。

与任何其他提供程序一样,此包将在 DbContextOptionsBuilder 上提供 UseCosmos 扩展方法。以此为基础,你需要提供 Azure Cosmos DB 连接字符串的三个关键元素:AccountEndpoint 值、Key 值和数据库名称。

可以从 Azure 门户、VS Code 的 Azure CosmosDB 扩展或适于 Visual Studio 2017 的 Cloud Explorer 扩展中复制连接字符串。UseCosmos 所需的格式与连接字符串的格式不同,但连接字符串确实提供了一个良好的开端。需要将三个值提供为三个逗号分隔的参数。图 1 显示了我在 ExpanseContext 类的 OnConfiguring 方法中直接配置连接的示例。

图 1 指定 DbContext 类中的 Cosmos DB 连接

using Expanse.Classes;
using Microsoft.EntityFrameworkCore;
public class ExpanseContext : DbContext
{
  public DbSet<Consortium> Consortia{get;set;}
  public DbSet<Planet> Planets { get; set; }
  public DbSet<Character> Characters { get; set; }
  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    optionsBuilder.UseCosmos(
      "https://lermandatapoints.documents.azure.com:443",
      "theverylongaccesskeygoeshere",
      "ExpanseCosmosDemo"
  );
}

连接参数是:

  • 帐户终结点
  • 键值的替代品以及
  • 数据库名称 ExpanseCosmosDemo

如果正在生成 ASP.NET Core 应用,则可以在 Startup 类的 ConfigureServices 方法中配置上下文,如下所示:

services.AddDbContext<ExpanseContext>(options=>
  options.UseCosmos(
    "https://lermandatapoints.documents.azure.com:443",
    "theverylongaccesskeygoeshere",
    "ExpanseCosmosDemo"
);

可以针对 Cosmos DB 数据库进行其他一些配置,但我想首先向你展示一些基本的默认行为。本文的第二部分将演示其他配置。

模型类

你需要熟悉我的模型,如图 2 中所示。Expanse 故事讲述的是两个相互竞争的联盟,即联合国和火星国会共和国。我将其简化以减少更改从属关系的复杂性,或者减少那些完全是恶意的角色的复杂性,并避开两个联盟。在模型中尊重这一点将需要介绍领域驱动设计 (DDD) 课程。

图 2 我的演示程序中的 Expanse 对象模型

public class Consortium
  {
    public Consortium()
    {
      Ships=new List<Ship>();
      Stations=new List<Station>();
    }
    public Guid ConsortiumId { get; set; }
    public string Name { get; set; }
    public List<Ship> Ships{get;set;}
    public List<Station> Stations{get;set;}
    public Origin Origin{get;set;  }
  }
  public class Planet
  {
    public Guid PlanetId { get; set; }
    public string PlanetName { get; set; }
  }
  public class Ship
  {
    public Guid ShipId {get;set;}
    public string ShipName {get;set;}
    public Guid PlanetId {get;set;}
    public Origin Origin{get;set;}
  }
  public class Origin
  {
    public DateTime Date{get;set;}
    public String Location{get;set;}
  }

这样一来,我就有了一些简单的类,同样,为了专注于提供程序的行为,我将这些类保留为没有实际业务逻辑的 CRUD 类。我将跳过更有趣的空间站和角色类,因为我不会在演示中使用它们。

除了配置提供程序之外,上下文类还为 Consortium 和 Ship 实体指定 DbSets。以下是我的上下文类 ExpanseContext 的完整列表:

public class ExpanseContext : DbContext
{
  public DbSet<Consortium> Consortia{get;set;}
  public DbSet<Ship> Ships { get; set; }
  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    optionsBuilder.UseCosmos(
      "https://lermandatapoints.documents.azure.com:443",
      "theverylongaccesskeygoeshere",
      "ExpanseCosmosDemo");
  }
}

让 EF Core 创建 Cosmos DB 数据库

将 Azure Cosmos DB 数据库与 Cosmos DB 帐户绑定,每个数据库将其文档存储在称为容器的各种子集中。可以将这些视为 Cosmos DB 中的集合,但最近对这一术语进行了更改。现在,将这些文档称为项目。但是,不要将容器视为关系数据库表。它们截然不同。该文档将它们称为“预配吞吐量和项目存储的可伸缩性单元”。 文档数据库不预定义数据结构,因此可以将结构不同的文档存储在单个容器中。如果你不熟悉文档数据库,请查看我之前的文章“文档数据库到底是什么?”,网址:msdn.com/magazine/hh547103。由于 Cosmos DB 专为存储大量数据而设计,因此确定不同容器和分区之间的平衡是一个重要的过程。在着手开始管理性能和成本之前,应该先熟悉它。当然,在详细了解数据使用方式的情况下,可以对这些方面进行微调。我建议观看“Azure Cosmos DB SQL API 的建模数据和最佳做法”视频,网址:bit.ly/2FZtIDs

EF Core Database.EnsureCreated 方法可以在 Azure 中创建 Azure Cosmos DB 数据库以及任何所需的容器。如果使用的是基于 Windows 的 Cosmos DB 模拟器 (bit.ly/2sHNsAn),则可以在开发期间以本地版本的数据库为目标。需要提前拥有一个现有的 Cosmos DB 帐户。在创建 Azure Cosmos DB 帐户和预配容器的吞吐量时,会做出需要做出的影响性能和成本的大多数重要决策。EF Core 可能正在创建新数据库或容器这一事实并不会强迫你接受某些未知的默认设置,但有两个默认设置需要注意:首先,EF Core 将所有文档存储在一个分区中,其次,它将使用 DbContext 的名称创建一个容器,将所有文档存储到该容器中。原因是数据库中的容器数量会影响成本。因此,如果还没有分发项目的计划,则可以在一个容器和一个分区中开始操作,然后根据需要配置其他分区容器以提高性能,同时,致力于在性能和成本之间取得平衡。

我的演示应用程序只是一个控制台应用。它的主要方法触发对我的 CreateDB 方法的调用,该方法调用 EF Core 的 EnsureCreated:

private static void CreateDB()
  {
    using(var context=new ExpanseContext())
    {
      context.Database.EnsureCreated();
    }
  }

因为我使用的是默认设置时,第一次运行 EnsureCreated 将创建新数据库和一个名为 Expanse­Context 的容器。如果你在操作过程中显式添加了其他容器,EnsureCreated 将识别数据库和初始容器已存在,并为你创建新容器。我将在本文的第二部分中更详细地介绍这一点。

将数据存储到容器中

自己的代码与 EF Core 的交互方式与跟任何其他数据库的交互方式相同。然而,有趣的是 EF Core 如何在后台与 Cosmos DB 进行交互。

例如,下面介绍一个方法,创建单个 Consortium 对象,将其附加到上下文并调用 SaveChanges:

private static void AddObject () {
  var consortium = new Consortium { ConsortiumId = Guid.NewGuid (),
    Name = "Martian Congressional Republic" };
  using (var context = new ExpanseContext ()) {
    context.Consortia.Add (consortium);
    context.SaveChanges ();
  }
}

该代码与你为关系数据库提供程序编写的代码相同。但是在基础 SaveChanges 方法中,EF Core 会将该对象转换为 JSON 对象,并且它是存储在数据库中的 JSON 数据。

但在将其发送到 Cosmos DB 之前,EF Core 会为 JSON 对象添加两个特殊属性。一个是名为 id 的属性,其值为 GUID。id 将确保容器中的每个项目都具有真正唯一的 ID,即使它们代表不同的实体。另一个添加的属性名为 Discriminator,包含此数据表示的实体类型的名称。EF Core 借助鉴别器能够区分项目所表示的实体类型。多亏 EF Core 的阴影属性功能 (bit.ly/2PjUq9k),它能够创建这些仅为 DbContext 所知的额外属性。这也意味着在查询数据时,会将阴影属性返回到 DbContext,但在具体化实体时会被忽略。

id 阴影属性不会降低 EF Core 的每个实体都需要一个键属性的要求。约定与以往一样。EF Core 仍然会查找名为 Id 或 [entity]Id 的键属性,在我的示例中,该键属性是 ConsortiumId 属性。并且随时可以使用映射替代该约定。

如果你使用的是关系数据库,则可能习惯于依赖它们为你生成键值。Cosmos DB 不是关系数据库,不会填充 ConsortiumId 或其他键。但是,EF Core 将为任何缺少的 GUID 键提供值。但是它没有适用于整数的键生成器,并且跟踪递增的整数并没有什么意思。因此,无论是计划提供键还是让 EF Core 执行此操作,我强烈建议使用 GUID。即使我没有执行此操作的真实用例,创建 ConsortiumId 值的 AddObject 方法也会执行此操作,因为我不会在其他地方使用该值。这只是为了演示。如果我在实例化 Consortium 时没有提供该值,那么 EF Core 将提供该值。请记住,如果使用 HasData 方法来播种数据,就像使用其他提供程序一样,需要指定主键属性和外键属性的值。

图 3 显示了作为 AddObject 方法的结果存储在容器中的项目。前四个属性来自 EF Core。但其他属性呢?Cosmos DB 总是添加供它在后台使用的许多元数据属性。但是,在使用 EF Core 查询该数据时,都不会将这些元数据属性返回到 EF Core 的 DbContext。

为 Cosmos DB 扩展显示的新联盟创建的项目
图 3 为 Cosmos DB 扩展显示的新联盟创建的项目

那些数据图呢?

联盟可以有一艘或多艘船舶。如果我创建一个拥有一艘船舶的新联盟,我的代码可能如下所示:

var consortium=new Consortium{ConsortiumId= Guid.NewGuid(),
  ConsortiumName="United Nations"};
consortium.Ships.Add(
  new Ship{ShipId=Guid.NewGuid(),ShipName="Canterbury"});

将联盟图添加到上下文并调用 SaveChanges 后,将向容器添加两个新项目,如图 4 中所示。

图 4 基于包含船舶的新联盟图创建的项目

{
  "ConsortiumId": "09bf2c04-e951-41d7-b890-ea5bc27b5766",
  "ConsortiumName": "United Nations Thursay",
  "Discriminator": "Consortium",
  "id": "fa479b49-144f-47ee-9761-e4f6dfe94cb2",
  "_rid": "Q0wDAKsiftgBAAAAAAAAAA==",
  "_self": "dbs/Q0wDAA==/colls/Q0wDAKsiftg=/docs/Q0wDAKsiftgBAAAAAAAAAA==/",
  "_etag": "\"000058c7-0000-0000-0000-5bf80dcf0000\"",
  "_attachments": "attachments/",
  "_ts": 1542983119
}
{
  "ShipId": "581a5c65-8df7-4479-8626-9d8fd2b1c4c7",
  "ConsortiumId": "09bf2c04-e951-41d7-b890-ea5bc27b5766",
  "Discriminator": "Ship",
  "PlanetId": 0,
  "ShipName": "Canterbury 3rd",
  "id": "ebc2dcda-efb5-451b-a65d-f6fa0bb011a4",
  "Origin": null,
  "_rid": "Q0wDAKsiftgCAAAAAAAAAA==",
  "_self": "dbs/Q0wDAA==/colls/Q0wDAKsiftg=/docs/Q0wDAKsiftgCAAAAAAAAAA==/",
  "_etag": "\"000059c7-0000-0000-0000-5bf80dd00000\"",
  "_attachments": "attachments/",
  "_ts": 1542983120
}

请注意,即使我没有在 Ship 类中定义 ConsortiumId 外键属性,EF Core 也知道需要跟踪关系,因此它通过应用外键值,使用船舶对象属于联盟对象的知识来完成它一直所执行的任务。根据我的业务逻辑,我通常使用和控制相关类型中的外键属性。但是如果你不这样做,提供程序也会为你执行此操作。

但 EF Core 不会执行的操作是创建一个 JSON 文档,并将一艘船舶作为联盟的子文档。这是因为两种类型都具有键属性,并且是 ExpanseContext 定义为实体数据模型中的真实实体,因此它们将始终表示为单个文档。

但是,EF Core 确实理解了分层 JSON 对象的概念,当在模型中使用自有实体时,你可以发现这一点。我将在第二篇文章中更详细地介绍此提供程序的自有实体。

从 Cosmos DB 检索数据

我将在本专栏中再介绍一个主题,快速介绍一下数据检索。

若要检索数据,可以像使用任何其他提供程序一样编写 LINQ 查询。EF Core 将使用 Cosmos DB SQL API 转换 LINQ 查询以获取项目。

EF Core 与其他提供程序一样,由你显式定义或由提供程序隐式定义的任何阴影属性(例如 Discriminator 和 id 属性)将始终作为条目的一部分返回到上下文。这样,EF Core 可以具体化正确的对象类型并维护每个对象的单个标识。

可以通过查询数据库中的数据后查询 ChangeTracker 条目来发现这一点,就像我在这里操作的一样:

private static void GetSomeDataBack () {
  using (var context = new ExpanseContext ()) {
    var consortia = context.Consortia.ToList ();
    var entries = context.ChangeTracker.Entries ().ToList ();
  }
}

通过深入了解其中一个条目的属性,可以看到存在五个属性(即使 Consortium 只有两个属性,即 ConsortiumId 和 Name)。请注意,甚至有元数据也说明它们是阴影属性。最后一个阴影属性是 __jObject,它包含条目的完整 JSON 对象的字符串表示形式。

查询相关数据

可以使用带有 Include 或投影的预先加载以及显式加载来加载相关数据。我测试了基于代理的延迟加载,它没有返回相关数据,并且被告知基于依赖注入的延迟加载此时也不起作用。 

图 5**** 中的三种方法显示了使用 Include、投影和显式加载,以及一些筛选来加载相关船舶的工作示例。代码与查询关系数据库提供程序没有什么不同。

图 5 加载相关数据的工作方式与使用 RDBMS 提供程序的方式相同

private static void EagerLoadInclude () {
  using (var context = new ExpanseContext ()) {
    var consortia = context.Consortia.Include (c => c.Ships).ToList ();
  }
}
private static void EagerLoadProjection () {
  using (var context = new ExpanseContext ()) {
    var consortia =
      context.Consortia.Select (c => new { c, c.Ships }).ToList ();
  }
}
private static void ExplicitLoad () {
  using (var context = new ExpanseContext ()) {
    var consortium =
      context.Consortia.FirstOrDefault (c => c.Name.Contains ("United"));
    context.Entry (consortium).Container (c => c.Ships).Load ();
  }
}

利用现有的 EF Core 知识来使用 Cosmos DB

尽管 Cosmos DB 是一种类型完全不同的数据存储(即存储 JSON 文档的文档数据库),与关系数据库完全不同,但可以利用现有的 EF Core 知识来使用它来存储和检索数据。但是,与任何数据库(关系数据库或非关系数据库)一样,仍需要在 EF Core 之外执行一些操作,以确保经济高效地使用数据库。

在本文的第二部分中,我将介绍一些更高级的功能,如配置容器和分区,将自有实体集成到混合环境中,以及使用日志记录来检查从 API 生成的一些 SQL。也许到那时,就可以通过新的预览版来实现其他功能。如果想密切关注提供程序的进度,那么 EF Core GitHub 存储库(网址:bit.ly/2rmUpYN)中提供一个出色的“命中列表”功能可供使用和考虑。


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

衷心感谢以下 Microsoft 技术专家对本文的审阅:Andriy Svyryd 和 Diego Vega