TripPin del 7 – Avanceret skema med M-typer
Bemærk
Dette indhold refererer i øjeblikket til indhold fra en ældre implementering til enhedstest i Visual Studio. Indholdet opdateres i den nærmeste fremtid for at dække den nye Power Query SDK-teststruktur.
Dette selvstudium i flere dele dækker oprettelsen af en ny datakildeudvidelse til Power Query. Selvstudiet er beregnet til at blive udført sekventielt – hver lektion bygger på den connector, der blev oprettet i tidligere lektioner, og føjer trinvist nye funktioner til din connector.
I denne lektion skal du:
- Gennemtving et tabelskema ved hjælp af M-typer
- Angiv typer for indlejrede poster og lister
- Refactor-kode til genbrug og enhedstest
I den forrige lektion har du defineret tabelskemaerne ved hjælp af et simpelt system af typen "Skematabel". Denne skematabelmetode fungerer for mange REST API'er/data Forbind orer, men tjenester, der returnerer komplette eller dybt indlejrede datasæt, kan drage fordel af tilgangen i dette selvstudium, som udnytter M-typesystemet.
Denne lektion fører dig gennem følgende trin:
- Tilføjer enhedstest.
- Definition af brugerdefinerede M-typer.
- Gennemtvingelse af et skema ved hjælp af typer.
- Omstrukturering af fælles kode i separate filer.
Tilføjelse af enhedstest
Før du begynder at bruge den avancerede skemalogik, skal du føje et sæt enhedstest til din connector for at reducere risikoen for utilsigtet at ødelægge noget. Enhedstest fungerer på følgende måde:
- Kopiér den fælles kode fra UnitTest-eksemplet til din
TripPin.query.pq
fil. - Føj en sektionserklæring til toppen af filen
TripPin.query.pq
. - Opret en delt post (kaldet
TripPin.UnitTest
). - Definer en
Fact
for hver test. - Kald
Facts.Summarize()
for at køre alle testene. - Referer til det forrige kald som den delte værdi for at sikre, at det evalueres, når projektet køres i 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];
Hvis du vælger Kør på projektet, evalueres alle fakta, og du får et rapportoutput, der ser sådan ud:
Ved hjælp af nogle principper fra testdrevet udvikling tilføjer du nu en test, der i øjeblikket mislykkes, men som snart bliver genimplementeret og rettet (ved udgangen af dette selvstudium). Du skal specifikt tilføje en test, der kontrollerer en af de indlejrede poster (mails), du får tilbage i Mennesker-enheden.
Fact("Emails is properly typed", type text, Type.ListItem(Value.Type(People{0}[Emails])))
Hvis du kører koden igen, kan du nu se, at du har en fejltest.
Nu skal du blot implementere funktionaliteten for at få dette til at fungere.
Definition af brugerdefinerede M-typer
Metoden til håndhævelse af skemaer i den forrige lektion brugte "skematabeller", der er defineret som navne-/typepar. Det fungerer godt, når du arbejder med flade/relationelle data, men understøtter ikke indstillingstyper for indlejrede poster/tabeller/lister eller giver dig mulighed for at genbruge typedefinitioner på tværs af tabeller/enheder.
I TripPin-tilfælde indeholder dataene i enhederne Mennesker og Lufthavne strukturerede kolonner og deler endda en type (Location
) for at repræsentere adresseoplysninger. I stedet for at definere navne-/typepar i en skematabel skal du definere hver af disse objekter ved hjælp af brugerdefinerede M-typeerklæringer.
Her er en hurtig opdatering af typer på M-sproget fra sprogspecifikationen:
En typeværdi er en værdi, der klassificerer andre værdier. En værdi, der er klassificeret af en type, siges at være i overensstemmelse med denne type. M-typesystemet består af følgende typer:
- Primitive typer, som klassificerer primitive værdier (
binary
,date
,datetime
,duration
datetimezone
,list
,logical
,null
,number
,record
,text
, ,time
)type
og også omfatter en række abstrakte typer (function
,table
,any
ognone
)- Posttyper, der klassificerer postværdier baseret på feltnavne og værdityper
- Listetyper, der klassificerer lister ved hjælp af en basistype for et enkelt element
- Funktionstyper, der klassificerer funktionsværdier baseret på typerne af deres parametre og returværdier
- Tabeltyper, der klassificerer tabelværdier baseret på kolonnenavne, kolonnetyper og nøgler
- Null-typer, der klassificerer værdien null ud over alle de værdier, der er klassificeret af en basistype
- Typetyper, der klassificerer værdier, der er typer
Ved hjælp af det rå JSON-output, du får (og/eller søger efter definitionerne i tjenestens $metadata), kan du definere følgende posttyper for at repræsentere komplekse OData-typer:
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
];
Bemærk, hvordan LocationType
refererer CityType
til og LocType
til at repræsentere de strukturerede kolonner.
For enheder på øverste niveau (som du vil have vist som tabeller) definerer du tabeltyper:
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
];
Du opdaterer derefter variablen SchemaTable
(som du bruger som en "opslagstabel" for objektet til at skrive tilknytninger) for at bruge disse nye typedefinitioner:
SchemaTable = #table({"Entity", "Type"}, {
{"Airlines", AirlinesType },
{"Airports", AirportsType },
{"People", PeopleType}
});
Gennemtvingelse af et skema ved hjælp af typer
Du er afhængig af en fælles funktion (Table.ChangeType
) til at gennemtvinge et skema på dine data, ligesom du brugte SchemaTransformTable
i den forrige lektion.
I modsætning til SchemaTransformTable
bruger du Table.ChangeType
en faktisk M-tabeltype som et argument og anvender dit skema rekursivt for alle indlejrede typer. Dens signatur ser sådan ud:
Table.ChangeType = (table, tableType as type) as nullable table => ...
Den komplette kodeliste for funktionen Table.ChangeType
findes i filen Table.ChangeType.pqm .
Bemærk
For fleksibilitet kan funktionen bruges i tabeller samt lister over poster (hvilket er den måde, tabeller repræsenteres på i et JSON-dokument).
Du skal derefter opdatere connectorkoden for at ændre schema
parameteren fra a table
til og type
føje et kald til Table.ChangeType
i 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
opdateres til at bruge listen over felter fra skemaet (for at kende navnene på, hvad der skal udvides, når du får vist resultaterne), men lader den faktiske håndhævelse af skemaet være 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];
Bekræfter, at indlejrede typer angives
Definitionen af dit PeopleType
nu angiver feltet Emails
til en liste over tekst ({text}
).
Hvis du anvender typerne korrekt, skal kaldet til Type.ListItem i din enhedstest nu returneres type text
i stedet type any
for .
Hvis du kører dine enhedstest igen, viser det, at de nu alle er forbi.
Omstrukturering af fælles kode i separate filer
Bemærk
M-programmet vil have forbedret understøttelse af henvisninger til eksterne moduler/fælles kode i fremtiden, men denne fremgangsmåde bør føre dig igennem indtil da.
På dette tidspunkt har din udvidelse næsten lige så meget "fælles" kode som TripPin-connectorkode. I fremtiden vil disse almindelige funktioner enten være en del af det indbyggede standardfunktionsbibliotek, eller du kan referere til dem fra en anden udvidelse. I øjeblikket omstrukturerer du din kode på følgende måde:
- Flyt funktionerne, der kan genbruges, til separate filer (.pqm).
- Angiv egenskaben Opret handling for filen til Kompiler for at sikre, at den inkluderes i filtypenavnet under buildet.
- Definer en funktion til indlæsning af koden ved hjælp af Expression.Evaluate.
- Indlæs hver af de almindelige funktioner, du vil bruge.
Den kode, der skal gøres, er inkluderet i kodestykket nedenfor:
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");
Konklusion
I dette selvstudium blev der foretaget en række forbedringer af den måde, du gennemtvinger et skema på de data, du får fra en REST API. Connectoren koder i øjeblikket skemaoplysningerne hårdt, hvilket har en ydeevnefordel på kørselstidspunktet, men den kan ikke tilpasse sig ændringerne i tjenestens metadataovertid. Fremtidige selvstudier flyttes til en rent dynamisk tilgang, der udleder skemaet fra tjenestens $metadata dokument.
Ud over skemaændringerne har dette selvstudium tilføjet Enhedstests for din kode og omstruktureret de almindelige hjælpefunktioner til separate filer for at forbedre den overordnede læsbarhed.