注
このコンテンツは現在、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 エンティティで受け取る入れ子のレコード、具体的には「Emails」の 1 つをチェックするテストを追加します。
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関数の完全なコード 一覧は、Table.ChangeType.pqm ファイルにあります。
注
柔軟性を高めるために、テーブルとレコードの一覧 (JSON ドキュメントでテーブルを表す方法) で関数を使用できます。
その後、コネクタ コードを更新して、schema パラメーターをtableからtypeに変更し、Table.ChangeTypeでGetEntityの呼び出しを追加する必要があります。
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) に移動します。
- ファイルの Build Action プロパティを Compile に設定して、ビルド中に拡張機能ファイルに含まれるようにします。
- 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");
Conclusion
このチュートリアルでは、REST API から取得するデータにスキーマを適用する方法を改善しました。 コネクタは現在、スキーマ情報をハード コーディングしており、実行時にはパフォーマンス上の利点がありますが、サービスのメタデータの超過時間の変更には適応できません。 今後のチュートリアルでは、サービスの $metadata ドキュメントからスキーマを推論する、純粋に動的なアプローチに移行します。
スキーマの変更に加えて、このチュートリアルではコードの単体テストを追加し、共通のヘルパー関数を個別のファイルにリファクタリングして、全体的な読みやすさを向上しました。