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 中,有三个对服务器的请求:
/Me
— 你请求的实际 URL。/$metadata
—OData.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 的“记录”值。
如果将输出窗口中的字段与原始 JSON 响应中返回的字段进行比较,则会发现不匹配。 查询结果有其他字段(Friends
、Trips
、GetFriendsTrips
),而这些字段并没有出现在 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
如果尚未将隐私级别设置设置为“始终忽略隐私级别设置”(也称为“快速组合”),则会出现隐私提示。
当你合并来自多个来源的数据,并且尚未为一个或多个来源指定隐私级别时,会出现隐私提示。 选择“继续”按钮,并将顶部源的隐私级别设置为“公共”。
选择“保存”,表格就会出现。 虽然这还不是导航表,但它提供了在后续课程中将其转换为导航表所需的基本功能。
从扩展内访问多个数据源时,不会进行数据组合检查。 由于从扩展内调用的所有数据源都继承了相同的授权上下文,因此可以假定它们是“安全”组合的。 当涉及数据组合规则时,扩展将始终被视为单一数据源。 将源与其他 M 源组合时,用户仍会收到常规隐私提示。
如果运行 Fiddler 并选择“查询编辑器”中的“刷新预览”按钮,则会发现导航表中每个项目都有单独的 Web 请求。 这表明正在进行迫切计算,而这在生成包含许多元素的导航表时并不理想。 后续课程演示了如何构建支持惰性计算的正确导航表。
结束语
本课程介绍了如何为 REST 服务构建简单的连接器。 在本例中,将现有 OData 扩展转换为标准 REST 扩展(使用 Web.Contents),但如果从头开始创建新的扩展,同样的概念也适用。
在下一课中,你将学习使用 Power BI Desktop 在本课中创建的查询,并将其转换为扩展中的真正导航表。