注释
此内容当前引用 Visual Studio 中用于单元测试的旧实现中的内容。 内容将在将来更新,以涵盖新的 Power Query SDK 测试框架。
此多部分教程介绍如何为 Power Query 创建新的数据源扩展。 本教程旨在按顺序完成 — 每个课程都基于在上一课中创建的连接器上构建,以增量方式向连接器添加新功能。
在本课中,你将:
- 使用 M 类型 强制实施表架构
- 设置嵌套记录和列表的类型
- 重构代码以供重用和单元测试
在上一课中,你使用简单的“架构表”系统定义了表架构。 此架构表方法适用于许多 REST API/数据连接器。 但是,返回完整数据集或深度嵌套数据集的服务可能受益于本教程中使用 M 类型系统的方法。
本课程指导你完成以下步骤:
- 添加单元测试。
- 定义自定义 M 类型。
- 使用类型强制实施架构。
- 将通用代码重构为单独的文件。
添加单元测试
在您开始使用高级模式逻辑之前,需要向连接器添加一组单元测试,以减少无意造成故障的机会。 单元测试的工作方式如下所示:
- 将 UnitTest 示例 中的常见代码复制到
TripPin.query.pq文件中。 - 添加节声明到文件
TripPin.query.pq的顶部。 - 创建 共享 记录(称为
TripPin.UnitTest)。 - 为每个测试定义一个
Fact。 - 调用
Facts.Summarize()以运行所有测试。 - 引用上一个调用作为共享值,以确保在 Visual Studio 中运行项目时对其进行评估。
section TripPinUnitTests;
shared TripPin.UnitTest =
[
// Put any common variables here if you only want them to be evaluated once
RootTable = TripPin.Contents(),
Airlines = RootTable{[Name="Airlines"]}[Data],
Airports = RootTable{[Name="Airports"]}[Data],
People = RootTable{[Name="People"]}[Data],
// Fact(<Name of the Test>, <Expected Value>, <Actual Value>)
// <Expected Value> and <Actual Value> can be a literal or let statement
facts =
{
Fact("Check that we have three entries in our nav table", 3, Table.RowCount(RootTable)),
Fact("We have Airline data?", true, not Table.IsEmpty(Airlines)),
Fact("We have People data?", true, not Table.IsEmpty(People)),
Fact("We have Airport data?", true, not Table.IsEmpty(Airports)),
Fact("Airlines only has 2 columns", 2, List.Count(Table.ColumnNames(Airlines))),
Fact("Airline table has the right fields",
{"AirlineCode","Name"},
Record.FieldNames(Type.RecordFields(Type.TableRow(Value.Type(Airlines))))
)
},
report = Facts.Summarize(facts)
][report];
在项目上选择“运行”将对所有的事实进行评估,并提供如下所示的报告输出:
借助测试驱动开发中的一些原则,现在添加一个当前失败的测试,该测试可以在本教程结束时重新实现并修复。 具体而言,您需要添加一个测试,用于检查返回到 People 实体中的嵌套记录之一(即电子邮件)。
Fact("Emails is properly typed", type text, Type.ListItem(Value.Type(People{0}[Emails])))
如果再次运行代码,现在应会看到测试失败。
现在,您只需实现该功能即可使其正常运作。
定义自定义 M 类型
上一课中的架构强制方法使用了定义为名称/类型对的“架构表”。 它适用于平面化/关系数据,但不支持在嵌套的记录/表/列表上设置类型,也不允许在多个表/实体之间重用类型定义。
在 TripPin 案例中,People 和 Airports 实体中的数据包含结构化列,甚至共享用于表示地址信息的类型(Location)。 你需要使用自定义 M 类型声明来定义每个实体,而不是在架构表中定义名称/类型对。
这里是来自语言规范的 M 语言中类型的快速回顾。
类型值是一个对其他值进行分类的值 。 认为按类型分类的值符合该类型。 M 类型系统由以下类型组成:
- 基元类型用于对基元值(
binary、date、datetime、datetimezone、duration、list、logical、null、number、record、text、time、type)进行分类,并且还包括许多抽象类型(function、table、any和none)。 - 记录类型(根据字段名称和值类型对记录值进行分类)
- 列表类型(使用单一项基类型对列表进行分类)
- 函数类型(根据其参数和返回值类型对函数值进行分类)
- 表类型(根据列名、列类型和键对表值进行分类)
- 可以为 null 的类型,在基类型所分类的所有值之外,还将 null 值进行分类
- 类型类型(对属于类型的值进行分类)
使用获取的原始 JSON 输出(以及/或查找 服务$metadata中的定义),可以定义以下记录类型来表示 OData 复杂类型:
LocationType = type [
Address = text,
City = CityType,
Loc = LocType
];
CityType = type [
CountryRegion = text,
Name = text,
Region = text
];
LocType = type [
#"type" = text,
coordinates = {number},
crs = CrsType
];
CrsType = type [
#"type" = text,
properties = record
];
请注意LocationType如何引用CityType和LocType来表示其结构化的列。
对于顶级实体(要表示为表),请定义 表类型:
AirlinesType = type table [
AirlineCode = text,
Name = text
];
AirportsType = type table [
Name = text,
IataCode = text,
Location = LocationType
];
PeopleType = type table [
UserName = text,
FirstName = text,
LastName = text,
Emails = {text},
AddressInfo = {nullable LocationType},
Gender = nullable text,
Concurrency = Int64.Type
];
然后更新 SchemaTable 变量(用作实体到类型映射的“查阅表”),以使用这些新类型定义:
SchemaTable = #table({"Entity", "Type"}, {
{"Airlines", AirlinesType },
{"Airports", AirportsType },
{"People", PeopleType}
});
使用类型约束模式
您依赖于一个通用函数(Table.ChangeType)来对数据强制实施架构,就像您在上一课中使用SchemaTransformTable的那样。 与 SchemaTransformTable 不同,Table.ChangeType 使用实际的 M 表类型作为参数,并将您的模式 以递归方式 应用于所有嵌套类型。 其签名如下所示:
Table.ChangeType = (table, tableType as type) as nullable table => ...
可以在 Table.ChangeType 文件中找到函数的完整代码列表。
注释
为获得灵活性,函数可用于表以及记录列表(即如何在 JSON 文档中表示表)。
然后,您需要更新连接器代码,将参数从 table 更改为 type,并在 GetEntity 中添加对 Table.ChangeType 的调用。
GetEntity = (url as text, entity as text) as table =>
let
fullUrl = Uri.Combine(url, entity),
schema = GetSchemaForEntity(entity),
result = TripPin.Feed(fullUrl, schema),
appliedSchema = Table.ChangeType(result, schema)
in
appliedSchema;
GetPage 将更新为使用架构中的字段列表(了解获取结果时要扩展的内容的名称),但会将实际架构强制设置为 GetEntity。
GetPage = (url as text, optional schema as type) as table =>
let
response = Web.Contents(url, [ Headers = DefaultRequestHeaders ]),
body = Json.Document(response),
nextLink = GetNextLink(body),
// If we have no schema, use Table.FromRecords() instead
// (and hope that our results all have the same fields).
// If we have a schema, expand the record using its field names
data =
if (schema <> null) then
Table.FromRecords(body[value])
else
let
// convert the list of records into a table (single column of records)
asTable = Table.FromList(body[value], Splitter.SplitByNothing(), {"Column1"}),
fields = Record.FieldNames(Type.RecordFields(Type.TableRow(schema))),
expanded = Table.ExpandRecordColumn(asTable, fields)
in
expanded
in
data meta [NextLink = nextLink];
正在确认嵌套类型是否已被设置
现在,您的 PeopleType 的定义将 Emails 字段设置为文本列表({text})。 如果正确应用类型,则对单元测试中的 Type.ListItem 的调用现在应返回type text而不是返回type any。
再次运行单元测试会显示它们现在全部通过。
将通用代码重构为单独的文件
注释
M 引擎将在未来改进对外部模块和通用代码引用的支持,但在此之前,这种方法可以确保满足您的需求。
此时,扩展几乎具有与 TripPin 连接器代码一样多的“通用”代码。 将来,这些 常见函数 要么是内置标准函数库的一部分,要么可以从另一个扩展引用它们。 现在,可以采用以下方式重构代码:
- 将可重用函数移动到单独的文件(.pqm)。
- 将文件的生成操作属性设置为编译,以确保它在生成过程中被包含在扩展文件中。
- 定义使用 Expression.Evaluate 加载代码的函数。
- 加载要使用的每个常用函数。
要执行此重构的代码包含在以下代码片段中:
Extension.LoadFunction = (fileName as text) =>
let
binary = Extension.Contents(fileName),
asText = Text.FromBinary(binary)
in
try
Expression.Evaluate(asText, #shared)
catch (e) =>
error [
Reason = "Extension.LoadFunction Failure",
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
Message.Parameters = {fileName, e[Reason], e[Message]},
Detail = [File = fileName, Error = e]
];
Table.ChangeType = Extension.LoadFunction("Table.ChangeType.pqm");
Table.GenerateByPage = Extension.LoadFunction("Table.GenerateByPage.pqm");
Table.ToNavigationTable = Extension.LoadFunction("Table.ToNavigationTable.pqm");
结论
本教程对从 REST API 获取的数据强制实施架构的方式进行了多项改进。 连接器当前将其模式信息硬编码,这在运行时具有性能优势,但无法随着时间的推移适应服务元数据的变化。 将来的教程将迁移到纯动态方法,该方法将从服务的$metadata文档中推断架构。
除了架构更改,本教程还为代码添加了单元测试,并将常见的帮助程序函数重构为单独的文件以提高整体可读性。