2019 年 3 月

第 34 卷,第 3 期

[数据点]

EF Core Cosmos DB 提供程序预览版速览(第 2 部分)

作者 Julie Lerman

Julie Lerman在 2019 年 1 月的“数据点”专栏中,我初探了 EF Core Cosmos DB 提供程序。此提供程序仍处于预览阶段,预计与 EF Core 3.0 版本一起发布,所以现在正是时候提前做好准备。

第 1 部分介绍了为什么要为 ORM 提供 NoSQL 提供程序,以及如何对各个对象及其相关数据执行一些基本的读取和写入操作(如简单模型所定义)。编写利用此提供程序的代码与使用更熟悉的关系数据库提供程序没有太大区别。

此外,其中还介绍了 EF Core 如何为已有 Azure 数据库帐户便捷创建数据库和容器,以及如何使用适用于 Visual Studio Code 的 Cosmos DB 扩展来查看云中的数据。

在 2019 年 2 月的专栏 (msdn.com/magazine/mt833267) 中,我转变为介绍 Azure Cosmos DB 的 MongoDB API,尽管这与 EF Core 不相关。现在,我将回归之前的主题,以分享我在探索 EF Core Cosmos DB 提供程序时的其他一些有趣发现。

本期专栏文章将介绍此提供程序的一些更高级功能,如将 DbContext 配置为更改 EF Core 定目标到 Cosmos DB 数据库容器的方式、实现包含从属实体的嵌入文档,以及使用 EF Core 日志记录查看 SQL 以及提供程序生成的其他有趣处理信息。

详细了解容器和 EF Core 映射

容器亦称为 Cosmos DB SQL 和 Mongo DB API 中的“集合”,它是对作为 Cosmos DB“基本可伸缩性单元”的项进行真正与架构无关的分组。也就是说,可以定义每容器吞吐量,容器也可以作为单元进行缩放和复制。随着数据增长,设计模型及其与容器的一致程度会影响性能和成本。如果已使用 EF 或 EF Core,便会熟悉一个 DbSet<TEntity> 映射到关系数据库中一个表的默认设置。不过,为每个 DbSet 单独创建 Cosmos DB 容器可能会是成本高昂的默认设置。但第 1 部分中所述的默认设置是,DbContext 的所有数据都映射到一个容器。约定是,容器与 DbContext 同名。

接下来看一看默认设置,以及可使用 EF Core 控制哪些设置。

在上一期专栏文章中,我让 EF Core 触发便捷新建数据库和容器。我已有定目标到(EF Core 使用的)SQL API 的 Azure 帐户。虽然异地冗余默认处于启用状态,但我仅将我的帐户配置为使用美国东部的一个数据中心。因此,多区域写入默认处于禁用状态。所以,不论我向此帐户添加什么数据库,以及向这些数据库添加任何容器,都将遵循此帐户控制的总体规范。

在第一列中,我有名为 ExpanseDbContext 的 DbContext。将 ExpanseDbContext 配置为使用 Cosmos 提供程序时,我指定了数据库名称应为 ExpanseCosmosDemo:

optionsBuilder.UseCosmos(endpointstring,accountkeystring, "ExpanseCosmosDemo")

当我的代码第一次对 ExpanseDbContext 实例调用 Database.EnsureCreated 时,ExpanseCosmosDemo 数据库与默认容器 ExpanseDbContext 一起创建,同时遵循使用 DbContext 类名称的约定。

容器是使用图 1 中的 Azure Cosmos DB 默认设置进行创建。图中未显示的是,使用默认设置的索引策略配置,也是“一致的”。

用于创建容器的 Azure Cosmos DB 默认设置
图 1:用于创建容器的 Azure Cosmos DB 默认设置

EF Core 无法影响这些设置。可以在门户中、使用 Azure CLI 或 SDK 修改它们。最好这样做,因为 EF Core 的作用是读取和写入数据。但 EF Core 可影响的一点是,要存储在不同容器中的容器名称和映射实体。

可使用 OnConfiguring 中的 HasDefaultContainerName 方法替代默认容器名称。例如,下面的示例使用 ExpanseDocuments 作为默认名称来替代 ExpanseDbContext:

modelBuilder.HasDefaultContainerName("ExpanseDocuments");

如果已决定要将数据拆分到不同的容器中,可以为特定实体映射新容器名称。下面的示例展示了如何将上一期专栏文章中的 Ship 实体指定到 ExpanseShips 容器中:

modelBuilder.Entity<Ship>().ToContainer("ExpanseShips");

可以根据需要将任意多个实体定目标到一个容器。默认容器已说明了这一点。但也可以根据需要结合使用 ToContainer(“ExpanseShips”) 与其他实体。

如果像这样将新容器添加到现有数据库,会发生什么情况?正如我在第 1 部分中所述,让 EF Core 创建数据库或容器的唯一方式是,调用 context.Data­base.EnsureCreated。EF Core 会识别已存在和尚不存在的容器,并根据需要新建任意容器。

如果你更改默认容器名称,EF 会新建容器,并在以后继续使用此容器。但原始容器中的所有数据都会保留在原地。

因为 Azure Cosmos DB 无法重命名现有容器,所以官方建议是将数据移到新集合中,可能是借助于批量执行程序库(如 bit.ly/2RbpTvp 中的库)。如果将实体更改为映射到其他容器,情况也是如此。原始数据不会移动,你要负责确保旧项转移。同样,更合理的做法可能是,在 EF Core 外部一次性移动数据。

我还测试了添加包含 Ship 的 Consortium 图,其中文档最终位于数据库中的单独容器内。读取此类数据时,我能够编写查询来查找主动加载自己 Ship 数据的 Consortia,例如:

context.Consortia.Include(c=>c.Ships).FirstOrDefault()

EF Core 能够从单独的容器中检索数据,并重新构造对象图。

将从属实体嵌入父文档中。

在第 1 部分中,可以看到这些相关实体存储在各自的文档中。我已在图 2 中列出了 Expanse 类,以提醒大家不要忘记示例模型。在我生成包含 Ship 的 Consortium 图时,每个对象都存储为单独文档,其中有可便于 EF Core 或其他代码重新连接它们的外键。这是一个非常相关的概念,但由于 Consortia 和 Ship 是有自己标识密钥的唯一实体,所以这就是 EF Core 暂留它们的方式。但 EF Core 确实理解文档数据库和嵌入文档,使用从属实体时可以见证这一点。请注意,Origin 类型没有键属性,它用作 Ship 和 Consortium 的属性。在我的模型中,它将是从属实体。若要详细了解 EF Core 从属实体功能,可以参阅我在 2018 年 4 月的“数据点”专栏文章 (msdn.com/magazine/mt846463)。

图 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 int PlanetId {get;set;}
  public Origin Origin{get;set;}
}
public class Origin
{
  public DateTime Date{get;set;}
  public String Location{get;set;}
}

为了让 EF Core 通过理解从属类型将它映射到数据库,你需要将它配置为数据注释或(始终是我的首选)Fluent API 配置。后一种配置在 DbContext OnConfiguring 方法中实现,如下所示:

modelBuilder.Entity<Ship>().OwnsOne(s=>s.Origin);
modelBuilder.Entity<Consortium>().OwnsOne(s=>s.Origin);
Here’s some code for adding a new Ship, along with its origin, to a consortium object:
consortium.Ships.Add(new Ship{ShipId=Guid.NewGuid(),ShipName="Nathan Hale 3rd",
                              Origin= new Origin {Date=DateTime.Now,
                              Location="Earth"}});

通过 ExpanseContext 保存 Consortium 后,新的 Ship 也会保存到它自己的文档中。

图 3**** 展示了 Origin 表示为嵌入文档的 Ship 文档。文档数据库不需要子文档,即可让外键随父文档一起返回。不过,用于暂留从属实体的 EF Core 逻辑确实需要外键(由 EF Core 卷影属性处理),才能暂留关系数据库中的从属实体。因此,它利用现有逻辑来推断 Origin 子文档中的 ShipId 属性。

图 3:嵌入 Origin 子文档的 Ship 文档

{
  "ShipId": "e5d48ffd-e52e-4d55-97c0-cee486a91629",
  "ConsortiumId": "60ccb22d-4422-45b2-a54a-71fa240435b3",
  "Discriminator": "Ship",
  "PlanetId": 0,
  "ShipName": "Nathan Hale 3rd",
  "id": "c2bdd90f-cb6a-4a3f-bacf-b0b3ac191662",
  "Origin": {
    "ShipId": "e5d48ffd-e52e-4d55-97c0-cee486a91629",
    "Date": "2019-01-22T11:40:29.117453-05:00",
    "Discriminator": "Origin",
    "Location": "Earth"
  },
  "_rid": "cgEVAKklUPgCAAAAAAAAAA==",
  "_self": "dbs/cgEVAA==/colls/cgEVAKklUPg=/docs/
            cgEVAKklUPgCAAAAAAAAAA==/",
  "_etag": "\"0000a43b-0000-0000-0000-5c47477d0000\"",
  "_attachments": "attachments/",
  "_ts": 1548175229
}

EF Core 还可以使用 OwnsMany 映射来映射从属集合。在这种情况下,数据库中的父文档内会有多个子文档。

有一个问题将在 EF Core 3.0.0 预览版 2 中进行修复。EF Core 暂不理解 null 从属实体属性。如果你尝试添加包含 null 从属实体属性的对象,其他数据库提供程序会抛出运行时异常。有关此行为,可以参阅上面提到的 2018 年 4 月专栏文章。遗憾的是,Cosmos DB 提供程序不阻止添加此状态下的对象,但它也无法具体化未填充从属实体属性的对象。以下是在此问题发生时抛出的异常:

"System.InvalidCastException: Unable to cast object of type
 'Newtonsoft.Json.Linq.JValue' to type 'Newtonsoft.Json.Linq.JObject'."

所以,如果你在尝试查询包含从属类型属性的实体时看到此错误,我希望你能记得 null 从属类型属性可能会导致异常抛出。

记录提供程序活动

如我在 2018 年 10 月的专栏文章 (msdn.com/magazine/mt830355) 中所述,EF Core 开始采用 .NET Core 日志记录框架。在这篇文章发布后不久,实例化 LoggerFactory 的语法得到了简化,但使用类别和日志级别确定应在日志中输出什么内容的方法并未更改。我在博客文章“EF Core 2.2 中的日志记录有更简单的语法 - 更像 ASP.NET Core”(bit.ly/2UdSkuI) 中报告了更新后的语法。

与 Cosmos DB 提供程序进行交互时,EF Core 也与记录器共享详细信息。也就是说,可以在日志中看到与其他提供程序相同的所有类型信息。

请注意,CosmosDB 不使用 SQL 执行插入、更新和删除操作,因为你习惯了使用关系数据库。由于 SQL 仅用于查询,因此 SaveChanges 不会在日志中显示 SQL。不过,可以查看 EF Core 如何修复对象并创建所需的任何 ID、外键和鉴别器。我能够在记录全部绑定到 Debug LogLevel 的类别时查看所有这些信息,而不是仅筛选数据库命令。

下面展示了我是如何配置 GetLoggerFactory 方法来实现此目的。请注意 AddFilter 方法。我使用的是空字符串(而不是将类别传递到第一个参数),这为我提供了所有类别:

private ILoggerFactory GetLoggerFactory()
{
  IServiceCollection serviceCollection = new ServiceCollection();
  serviceCollection.AddLogging(builder =>
         builder.AddConsole()
                .AddFilter("" , LogLevel.Debug));
  return serviceCollection.BuildServiceProvider()
          .GetService<ILoggerFactory>();
}

如果我只想筛选 SQL 命令,我会传递 DbLoggerCategory.Database.Command.Name,以仅提供这些事件的正确字符串,而不是提供空字符串。在插入几个图并执行单个查询以检索一些已插入数据时,这会中继大量日志记录消息。我将在本专栏随附的下载内容中添加完整输出和我的程序。

这些日志中的一些有趣信息包括,当可以在此提供程序中看到特殊 Discriminator 属性被填充时,添加卷影属性:

dbug: Microsoft.EntityFrameworkCore.Model[10600]
      The property 'Discriminator' on entity type 'Station' was created in shadow state
      because there are no eligible CLR members with a matching name.

如果正在保存数据,那么在执行所有修复操作后,便会看到 SaveChanges 要启动的日志消息:

debug: Microsoft.EntityFrameworkCore.Update[10004]
       SaveChanges starting for 'ExpanseContext'.

后跟关于要调用的 DetectChanges 的消息。提供程序使用内部 API 逻辑来添加、修改或删除相关集合中的文档,但你不会看到相应操作的任何特定日志。不过,在这些操作完成后,日志便会中继典型的保存后步骤,如更新刚发布对象的状态的上下文:

dbug: Microsoft.EntityFrameworkCore.ChangeTracking[10807]
      The 'Consortium' entity with key '{ConsortiumId: a4b0405e-a820-4806-8b60-159033184cf1}' 
      tracked by 'ExpanseContext' changed from 'Added' to 'Unchanged'.

若要执行查询,便会在 EF Core 处理查询时看到大量消息。EF Core 先编译查询,再将它处理为消息,直到它到达发送到数据库的 SQL。以下是显示最终 SQL 的日志消息:

dbug: Microsoft.EntityFrameworkCore.Database.Command[30000]
      Executing Sql Query [Parameters=[]]
      SELECT c
      FROM root c
      WHERE (c["Discriminator"] = "Consortium")

等待发布

EF Core Cosmos DB 提供程序预览版适用于 EF Core 2.2 及更高版本。我使用的是 EF Core 2.2.1,后来为了确定我是否注意到任何更改,我切换到了最新 EF Core 3 预览版(版本 3.0.0-preview.18572.1)中未发布的 EF Core 包。

EF Core 3 的发布日程安排与 .NET Core 3.0 相同,但有关发行时间的最新信息只提及“在 2019 年的某个时间”。 2019 年 1 月末的博客文章 (bit.ly/2UsNBp6) 中宣布了正式发布预览版 2。如果你有意探索对 Azure Cosmos DB 的此支持,我建议立即试用它,并帮助 EF 团队发现任何问题,以让提供程序在发布时更可行。


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

衷心感谢以下 Microsoft 技术专家对本文的审阅:Andriy Svyryd
Andriy Svyryd 是 Microsoft 开发人员,专注于数据建模和 API 设计。  自 2010 年以来,他一直都是实体框架团队的开发人员。有关他的作品和个人项目,可以访问 https://github.com/AndriySvyryd。若要查看他的完整个人简介,可以访问 https://www.linkedin.com/in/andriy-svyryd-51364719/