TripPin 第 2 部分 - REST 服务的数据连接器

本教程分为多个部分,介绍如何针对 Power Query 创建新数据源扩展。 本教程按顺序进行,每一课都建立在前几课创建的连接器的基础上,逐步为连接器添加新功能。

在本课中,你将:

  • 使用 Web.Contents 创建调用 REST API 的基本函数
  • 了解如何设置请求标头和处理 JSON 响应
  • 使用 Power BI Desktop 将响应处理成用户友好的格式

本课将 TripPin 服务基于 OData 的连接器(在上一课中创建)转换为类似于为任何 RESTful API 创建的连接器。 OData 是 RESTful API,但具有固定约定集。 OData 的优势在于它提供架构、数据检索协议和标准查询语言。 如果不使用 OData.Feed,我们就需要自己在连接器中构建这些功能。

回顾 OData 连接器

在从连接器中删除 OData 函数之前,让我们快速回顾一下连接器目前(主要在幕后)从服务中检索数据的工作。

在 Visual Studio Code 中从第 1 部分打开 TripPin 扩展项目。 打开查询文件并粘贴以下查询:

TripPin.Feed("https://services.odata.org/v4/TripPinService/Me")

打开 Fiddler,然后在 Visual Studio Code 中评估当前 Power Query 文件。

在 Fiddler 中,有三个对服务器的请求:

Fiddler OData 请求。

  • /Me — 你请求的实际 URL。
  • /$metadataOData.Feed 函数自动进行的调用,可确定响应的架构和类型信息。
  • /Me/BestFriend—列出 /Me 单一实例时,(急切地)拉取的其中一个字段。 在这种情况下,调用会产生 204 No Content 状态。

M 评估大多是懒惰的。 大多数情况下,只有在需要时才会检索/拉取数据值。 在某些情况下(如 /Me/BestFriend 案例),会急切地拉取数据值。 这种情况往往发生在需要成员的类型信息时,而引擎除了检索值并检查之外,别无他法确定其类型。 让事情变得懒惰(即避免急切拉取)是使 M 连接器具有良好性能的关键因素之一。

请注意与请求一起发送的请求标头和 /Me 请求响应的 JSON 格式。

{
  "@odata.context": "https://services.odata.org/v4/TripPinService/$metadata#Me",
  "UserName": "aprilcline",
  "FirstName": "April",
  "LastName": "Cline",
  "MiddleName": null,
  "Gender": "Female",
  "Age": null,
  "Emails": [ "April@example.com", "April@contoso.com" ],
  "FavoriteFeature": "Feature1",
  "Features": [ ],
  "AddressInfo": [
    {
      "Address": "P.O. Box 555",
      "City": {
        "Name": "Lander",
        "CountryRegion": "United States",
        "Region": "WY"
      }
    }
  ],
  "HomeAddress": null
}

查询完成评估后,PQTest 结果窗口应显示 Me singleton 的“记录”值。

OData 结果。

如果将输出窗口中的字段与原始 JSON 响应中返回的字段进行比较,则会发现不匹配。 查询结果有其他字段(FriendsTripsGetFriendsTrips),而这些字段并没有出现在 JSON 响应的任何位置。 OData.Feed 函数根据$ metadata 返回的架构自动将这些字段添加到记录中。 这是一个很好的例子,说明连接器如何增强和/或重新设置服务响应格式以提供更好的用户体验。

创建基本 REST 连接器

现在,你将向调用 Web.Contents 的连接器添加新的导出函数。

不过,为了能够成功向 OData 服务发出 Web 请求,必须设置一些标准 OData 标头。 为此,你需要在连接器中定义一组通用的标头作为新变量:

DefaultRequestHeaders = [
    #"Accept" = "application/json;odata.metadata=minimal",  // column name and values only
    #"OData-MaxVersion" = "4.0"                             // we only support v4
];

你会更改 TripPin.Feed 函数的实现,以便使用 Web.Contents 而非 OData.Feed 进行网络请求,并将结果解析为 JSON 文档。

TripPinImpl = (url as text) =>
    let
        source = Web.Contents(url, [ Headers = DefaultRequestHeaders ]),
        json = Json.Document(source)
    in
        json;

在对连接器文件进行更改之后,请记得构建连接器。 然后,可以评估查询文件 (TripPin.query.pq)。 /Me 记录的结果现在与你在 Fiddler 请求中看到的原始 JSON 很相似。

如果在运行新函数时观察 Fiddler,你还会注意到现在评估只进行一次 Web 请求,而不是三次。 恭喜,你已经实现了 300% 的性能提升! 现在已经丢失了所有类型和架构信息,但还没有必要关注该部分。

更新查询以访问某些 TripPin 实体/表,例如:

  • https://services.odata.org/v4/TripPinService/Airlines
  • https://services.odata.org/v4/TripPinService/Airports
  • https://services.odata.org/v4/TripPinService/Me/Trips

你会注意到,以前返回格式良好的表的路径现在返回带有嵌入 [List] 的顶级“值”字段。 你需要对结果执行一些转换,使其适用于最终用户消耗应用场景。

列出结果。

在 Power Query 中创建转换

当然可以手动创作 M 转换,但大多数人更喜欢使用 Power Query 来塑造其数据。 你可以在 Power BI Desktop 中打开扩展,然后使用它设计查询,将输出转换为对用户更友好的格式。 重新生成解决方案,将新扩展文件复制到自定义数据连接器目录,然后重新启动 Power BI Desktop。

启动新的空白查询,然后将以下内容粘贴到编辑栏中:

= TripPin.Feed("https://services.odata.org/v4/TripPinService/Airlines")

请确保包含 = 符号。

操作输出,直到它看起来像原始 OData 源,一个包含两列的表:AirlineCode 和 Name。

格式化的航空公司。

生成的查询应如下所示:

let
    Source = TripPin.Feed("https://services.odata.org/v4/TripPinService/Airlines"),
    value = Source[value],
    toTable = Table.FromList(value, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
    expand = Table.ExpandRecordColumn(toTable, "Column1", {"AirlineCode", "Name"}, {"AirlineCode", "Name"})
in
    expand

为查询命名(“航空公司”)。

创建新的空白项目。 这次,使用 TripPin.Feed 函数访问 /Airports 实体。 应用转换,直到获得类似于下面所示的共享内容。 也可以在下面找到匹配的查询,并为此查询命名(“机场”)。

格式化的机场。

let
    Source = TripPin.Feed("https://services.odata.org/v4/TripPinService/Airports"),
    value = Source[value],
    #"Converted to Table" = Table.FromList(value, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
    #"Expanded Column1" = Table.ExpandRecordColumn(#"Converted to Table", "Column1", {"Name", "IcaoCode", "IataCode", "Location"}, {"Name", "IcaoCode", "IataCode", "Location"}),
    #"Expanded Location" = Table.ExpandRecordColumn(#"Expanded Column1", "Location", {"Address", "Loc", "City"}, {"Address", "Loc", "City"}),
    #"Expanded City" = Table.ExpandRecordColumn(#"Expanded Location", "City", {"Name", "CountryRegion", "Region"}, {"Name.1", "CountryRegion", "Region"}),
    #"Renamed Columns" = Table.RenameColumns(#"Expanded City",{{"Name.1", "City"}}),
    #"Expanded Loc" = Table.ExpandRecordColumn(#"Renamed Columns", "Loc", {"coordinates"}, {"coordinates"}),
    #"Added Custom" = Table.AddColumn(#"Expanded Loc", "Latitude", each [coordinates]{1}),
    #"Added Custom1" = Table.AddColumn(#"Added Custom", "Longitude", each [coordinates]{0}),
    #"Removed Columns" = Table.RemoveColumns(#"Added Custom1",{"coordinates"}),
    #"Changed Type" = Table.TransformColumnTypes(#"Removed Columns",{{"Name", type text}, {"IcaoCode", type text}, {"IataCode", type text}, {"Address", type text}, {"City", type text}, {"CountryRegion", type text}, {"Region", type text}, {"Latitude", type number}, {"Longitude", type number}})
in
    #"Changed Type"

可以对服务下的更多路径重复此过程。 准备就绪后,进入下一步,创建(模拟)导航表。

模拟导航表

现在,将生成一个表(使用 M 代码)来展示格式良好的 TripPin 实体。

新建空白查询并打开高级编辑器。

粘贴以下查询:

let
    source = #table({"Name", "Data"}, {
        { "Airlines", Airlines },
        { "Airports", Airports }
    })
in
    source

如果尚未将隐私级别设置设置为“始终忽略隐私级别设置”(也称为“快速组合”),则会出现隐私提示。

防火墙。

当你合并来自多个来源的数据,并且尚未为一个或多个来源指定隐私级别时,会出现隐私提示。 选择“继续”按钮,并将顶部源的隐私级别设置为“公共”。

隐私。

选择“保存”,表格就会出现。 虽然这还不是导航表,但它提供了在后续课程中将其转换为导航表所需的基本功能。

FakeNav。

从扩展内访问多个数据源时,不会进行数据组合检查。 由于从扩展内调用的所有数据源都继承了相同的授权上下文,因此可以假定它们是“安全”组合的。 当涉及数据组合规则时,扩展将始终被视为单一数据源。 将源与其他 M 源组合时,用户仍会收到常规隐私提示。

如果运行 Fiddler 并选择“查询编辑器”中的“刷新预览”按钮,则会发现导航表中每个项目都有单独的 Web 请求。 这表明正在进行迫切计算,而这在生成包含许多元素的导航表时并不理想。 后续课程演示了如何构建支持惰性计算的正确导航表。

结束语

本课程介绍了如何为 REST 服务构建简单的连接器。 在本例中,将现有 OData 扩展转换为标准 REST 扩展(使用 Web.Contents),但如果从头开始创建新的扩展,同样的概念也适用。

在下一课中,你将学习使用 Power BI Desktop 在本课中创建的查询,并将其转换为扩展中的真正导航表。

后续步骤

TripPin 第 3 部分 - 导航表