TripPin Teil 7 - Erweitertes Schema mit M-Typen
Hinweis
Dieser Inhalt verweist derzeit auf Inhalte aus einer Vorversion-Implementierung für Unittest in Visual Studio. Der Inhalt wird in Naher Zukunft aktualisiert, um das neue Power Query SDK-Testframework abzudecken.
Dieser mehrteilige Lehrgang behandelt die Erstellung einer neuen Datenquellenerweiterung für Power Query. Der Lehrgang sollte nacheinander durchgeführt werden - jede Lektion baut auf dem in den vorangegangenen Lektionen erstellten Connector auf und fügt dem Connector schrittweise neue Funktionen hinzu.
In dieser Lektion lernen Sie Folgendes:
- Erzwingen eines Tabellenschemas mit M-Typen
- Typen für verschachtelte Datensätze und Listen festlegen
- Refaktorierung von Code für Wiederverwendung und Unit-Tests
In der vorangegangenen Lektion haben Sie Ihre Tabellenschemata mit Hilfe eines einfachen "Schema Table"-Systems definiert. Dieser Schema-Tabellen-Ansatz funktioniert für viele REST-APIs/Data Connectors, aber Dienste, die vollständige oder tief verschachtelte Datensätze zurückgeben, könnten von dem Ansatz in diesem Tutorial profitieren, der das M-Typ-Systemnutzt.
In dieser Lektion werden Sie durch die folgenden Schritte geführt:
- Hinzufügen von Einheitstests.
- Definition von benutzerdefinierten M-Typen.
- Erzwingen eines Schemas mit Hilfe von Typen.
- Refactoring von gemeinsamem Code in separate Dateien.
Hinzufügen von Einheitstests
Bevor Sie mit der erweiterten Schemalogik beginnen, fügen Sie Ihrem Connector eine Reihe von Unit-Tests hinzu, um die Wahrscheinlichkeit zu verringern, dass Sie versehentlich etwas kaputt machen. Unit-Tests funktionieren folgendermaßen:
- Kopieren Sie den allgemeinen Code aus dem UnitTest-Beispiel in Ihre
TripPin.query.pq
-Datei. - Fügen Sie am Anfang der Datei
TripPin.query.pq
eine Abschnittserklärung hinzu. - Erstellen Sie einen gemeinsamen Datensatz (genannt
TripPin.UnitTest
). - Definieren Sie für jeden Test eine
Fact
. - Rufen Sie
Facts.Summarize()
auf, um alle Tests durchzuführen. - Verweisen Sie auf den vorherigen Aufruf als gemeinsamen Wert, um sicherzustellen, dass er ausgewertet wird, wenn das Projekt in Visual Studio ausgeführt wird.
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];
Wenn Sie für das Projekt "Ausführen" wählen, werden alle Fakten ausgewertet und Sie erhalten einen Bericht, der etwa so aussieht:
Unter Verwendung einiger Prinzipien aus test-driven developmentfügen Sie nun einen Test hinzu, der derzeit fehlschlägt, aber bald neu implementiert und korrigiert werden wird (bis zum Ende dieses Tutorials). Insbesondere fügen Sie einen Test hinzu, der einen der verschachtelten Datensätze (Emails) prüft, die Sie in der Entität Personen zurückerhalten.
Fact("Emails is properly typed", type text, Type.ListItem(Value.Type(People{0}[Emails])))
Wenn Sie den Code erneut ausführen, sollten Sie jetzt sehen, dass der Test fehlschlägt.
Jetzt müssen Sie nur noch die Funktionalität implementieren, damit dies funktioniert.
Definition von benutzerdefinierten M-Typen
Der Ansatz zur Schemaerzwingung in der vorherigen Lektion verwendet "Schematabellen", die als Name/Typ-Paare definiert sind. Es funktioniert gut, wenn man mit flattened/relationalen Daten arbeitet, aber es unterstützt nicht das Setzen von Typen für verschachtelte Datensätze/Tabellen/Listen oder erlaubt die Wiederverwendung von Typdefinitionen über Tabellen/Entitäten hinweg.
Im Fall von TripPin enthalten die Daten in den Entitäten "Personen" und "Flughäfen" strukturierte Spalten und haben sogar einen gemeinsamen Typ (Location
) zur Darstellung von Adressinformationen. Anstatt Name/Typ-Paare in einer Schematabelle zu definieren, werden Sie jede dieser Entitäten mit benutzerdefinierten M-Typ-Deklarationen definieren.
Hier ist eine kurze Auffrischung über Typen in der Sprache M aus der Language Specification:
Ein Typwert ist ein Wert, der andere Werte klassifiziert. Ein Wert, der durch einen Typ klassifiziert wird, wird als konform mit diesem Typ bezeichnet. Das M-Typsystem besteht aus den folgenden Arten von Typen:
- Primitive Typen, die primitive Werte klassifizieren (
binary
,date
,datetime
,datetimezone
,duration
,list
,logical
,null
,number
,record
,text
,time
,type
) und auch eine Reihe von abstrakten Typen umfassen (function
,table
,any
undnone
)- Datensatztypen, die Datensatzwerte basierend auf Feldnamen und Werttypen klassifizieren
- Listentypen, die Listen mithilfe eines einzelnen Elementbasistypen klassifizieren
- Funktionstypen, die Funktionswerte basierend auf den Typen ihrer Parameter und Rückgabewerte klassifizieren
- Tabellentypen, die Tabellenwerte basierend auf Spaltennamen, Spaltentypen und Schlüsseln klassifizieren
- Nullable-Typen, die zusätzlich zu allen von einem Basistyp klassifizierten Werten den Wert NULL klassifizieren
- Typentypen, die Werte klassifizieren, bei denen es sich um Typen handelt
Anhand der rohen JSON-Ausgabe, die Sie erhalten (und/oder durch Nachschlagen der Definitionen in den $metadata des -Dienstes), können Sie die folgenden Datensatztypen definieren, um komplexe OData-Typen darzustellen:
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
];
Beachten Sie, dass LocationType
auf CityType
und LocType
verweist, um seine strukturierten Spalten darzustellen.
Für die Entitäten der obersten Ebene (die als Tabellen dargestellt werden sollen), definieren Sie Tabellentypen:
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
];
Anschließend aktualisieren Sie Ihre Variable SchemaTable
(die Sie als "Nachschlagetabelle" für die Zuordnungen von Entitäten zu Typen verwenden), um diese neuen Typdefinitionen zu verwenden:
SchemaTable = #table({"Entity", "Type"}, {
{"Airlines", AirlinesType },
{"Airports", AirportsType },
{"People", PeopleType}
});
Erzwingen eines Schemas mit Hilfe von Typen
Sie werden sich auf eine allgemeine Funktion (Table.ChangeType
) verlassen, um ein Schema für Ihre Daten zu erzwingen, ähnlich wie Sie SchemaTransformTable
in der vorherigen Lektion verwendet haben.
Im Gegensatz zu SchemaTransformTable
nimmt Table.ChangeType
einen tatsächlichen M-Tabellentyp als Argument an und wendet Ihr Schema rekursiv für alle verschachtelten Typen an. Seine Signatur sieht so aus:
Table.ChangeType = (table, tableType as type) as nullable table => ...
Das vollständige Codelisting für die Funktion Table.ChangeType
finden Sie in der Datei Table.ChangeType.pqm.
Hinweis
Aus Gründen der Flexibilität kann die Funktion sowohl auf Tabellen als auch auf Listen von Datensätzen angewendet werden (so würden Tabellen in einem JSON-Dokument dargestellt werden).
Sie müssen dann den Connectorcode aktualisieren, um den Parameter schema
von table
in type
zu ändern, und einen Aufruf von Table.ChangeType
in GetEntity
hinzufügen.
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
wird aktualisiert, um die Liste der Felder aus dem Schema zu verwenden (um die Namen der Felder zu kennen, die erweitert werden sollen, wenn Sie die Ergebnisse erhalten), überlässt aber die eigentliche Schemaerzwingung 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];
Bestätigen, dass verschachtelte Typen eingestellt werden
Die Definition für Ihr PeopleType
setzt nun das Feld Emails
auf eine Liste von Text ({text}
).
Wenn Sie die Typen korrekt anwenden, sollte der Aufruf von Type.ListItem in Ihrem Unit-Test nun type text
statt type any
zurückgeben.
Wenn Sie Ihre Unit-Tests erneut ausführen, zeigt sich, dass sie jetzt alle erfolgreich sind.
Refaktorierung gemeinsamen Codes in separate Dateien
Hinweis
Die M-Engine wird in Zukunft eine bessere Unterstützung für die Referenzierung externer Module/gemeinsamen Codes bieten, aber bis dahin sollte dieser Ansatz ausreichen.
Zu diesem Zeitpunkt hat Ihre Erweiterung fast so viel "gemeinsamen" Code wie der TripPin-Verbindungscode. In Zukunft werden diese gemeinsamen Funktionen entweder Teil der eingebauten Standard-Funktionsbibliothek sein, oder man wird sie von einer anderen Erweiterung aus referenzieren können. Für den Moment überarbeiten Sie Ihren Code auf folgende Weise:
- Verschieben Sie die wiederverwendbaren Funktionen in separate Dateien (.pqm).
- Setzen Sie die Eigenschaft Build Action der Datei auf Compile, um sicherzustellen, dass sie während des Builds in Ihre Erweiterungsdatei aufgenommen wird.
- Definieren Sie eine Funktion zum Laden des Codes mit Expression.Evaluate.
- Laden Sie jede der allgemeinen Funktionen, die Sie verwenden möchten.
Der entsprechende Code ist im folgenden Ausschnitt enthalten:
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");
Zusammenfassung
Dieses Tutorial hat eine Reihe von Verbesserungen an der Art und Weise vorgenommen, wie Sie ein Schema für die Daten erzwingen, die Sie von einer REST-API erhalten. Der Connector kodiert seine Schemainformationen derzeit fest, was zwar zur Laufzeit einen Leistungsvorteil bringt, aber nicht in der Lage ist, sich im Laufe der Zeit an Änderungen der Metadaten des Dienstes anzupassen. Zukünftige Tutorials werden zu einem rein dynamischen Ansatz übergehen, der das Schema aus dem $metadata-Dokument des Dienstes ableitet.
Zusätzlich zu den Schemaänderungen wurden in diesem Tutorial Unit Tests für Ihren Code hinzugefügt und die allgemeinen Hilfsfunktionen in separate Dateien umstrukturiert, um die allgemeine Lesbarkeit zu verbessern.