TripPin del 7 – Avancerat schema med M-typer
Kommentar
Det här innehållet refererar för närvarande till innehåll från en äldre implementering för enhetstestning i Visual Studio. Innehållet uppdateras inom en snar framtid för att täcka det nya Power Query SDK-testramverket.
Den här självstudien i flera delar beskriver hur du skapar ett nytt datakällans tillägg för Power Query. Självstudien är avsedd att utföras sekventiellt – varje lektion bygger på anslutningsappen som skapades i föregående lektioner och lägger stegvis till nya funktioner i anslutningsappen.
I den här lektionen kommer du att:
- Framtvinga ett tabellschema med hjälp av M-typer
- Ange typer för kapslade poster och listor
- Omstrukturera kod för återanvändning och enhetstestning
I föregående lektion definierade du dina tabellscheman med hjälp av ett enkelt schematabellsystem. Den här schematabellmetoden fungerar för många REST-API:er/data Anslut orer, men tjänster som returnerar fullständiga eller djupt kapslade datamängder kan dra nytta av metoden i den här självstudien, som utnyttjar M-typsystemet.
Den här lektionen vägleder dig genom följande steg:
- Lägga till enhetstester.
- Definiera anpassade M-typer.
- Framtvinga ett schema med hjälp av typer.
- Omstrukturera vanlig kod till separata filer.
Lägga till enhetstester
Innan du börjar använda avancerad schemalogik lägger du till en uppsättning enhetstester i anslutningsappen för att minska risken för att oavsiktligt bryta något. Enhetstestning fungerar så här:
- Kopiera den vanliga koden från UnitTest-exemplet till filen
TripPin.query.pq
. - Lägg till en avsnittsdeklaration överst
TripPin.query.pq
i filen. - Skapa en delad post (kallas
TripPin.UnitTest
). - Definiera en
Fact
för varje test. - Anropa
Facts.Summarize()
för att köra alla tester. - Referera till föregående anrop som det delade värdet för att se till att det utvärderas när projektet körs 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];
Om du väljer kör i projektet utvärderas alla fakta och du får ett rapportutdata som ser ut så här:
Med hjälp av vissa principer från testdriven utveckling lägger du nu till ett test som för närvarande misslyckas, men som snart kommer att omimplementeras och åtgärdas (i slutet av den här självstudien). Mer specifikt lägger du till ett test som kontrollerar en av de kapslade posterna (e-postmeddelanden) som du får tillbaka i den Personer entiteten.
Fact("Emails is properly typed", type text, Type.ListItem(Value.Type(People{0}[Emails])))
Om du kör koden igen bör du nu se att du har ett misslyckat test.
Nu behöver du bara implementera funktionerna för att det här ska fungera.
Definiera anpassade M-typer
Metoden för schematillämpning i föregående lektion använde "schematabeller" som definierats som Namn/Typ-par. Det fungerar bra när du arbetar med utplattade/relationsdata, men har inte stöd för inställningstyper på kapslade poster/tabeller/listor, eller så kan du återanvända typdefinitioner mellan tabeller/entiteter.
I TripPin-fallet innehåller data i entiteterna Personer och Airports strukturerade kolumner och delar till och med en typ (Location
) för att representera adressinformation. I stället för att definiera namn/typpar i en schematabell definierar du var och en av dessa entiteter med hjälp av anpassade M-typdeklarationer.
Här är en snabb uppdatering om typer i M-språket från språkspecifikationen:
Ett typvärde är ett värde som klassificerar andra värden. Ett värde som klassificeras av en typ sägs överensstämma med den typen. M-typsystemet består av följande typer:
- Primitiva typer, som klassificerar primitiva värden (
binary
, ,datetime
date
,datetimezone
,duration
,list
,logical
,null
,number
,record
,text
, ,time
)type
och även innehåller ett antal abstrakta typer (function
,table
,any
ochnone
)- Posttyper, som klassificerar postvärden baserat på fältnamn och värdetyper
- Listtyper, som klassificerar listor med en bastyp för ett objekt
- Funktionstyper som klassificerar funktionsvärden baserat på typerna av deras parametrar och returnerar värden
- Tabelltyper som klassificerar tabellvärden baserat på kolumnnamn, kolumntyper och nycklar
- Nullbara typer, som klassificerar värdet null utöver alla värden som klassificeras av en bastyp
- Typtyper som klassificerar värden som är typer
Med hjälp av de råa JSON-utdata som du får (och/eller letar upp definitionerna i tjänstens $metadata) kan du definiera följande posttyper för att representera OData-komplexa 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
];
Observera hur LocationType
refererar till CityType
och LocType
för att representera dess strukturerade kolumner.
För de översta entiteterna (som du vill ska representeras som tabeller) definierar du tabelltyper:
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
];
Sedan uppdaterar SchemaTable
du variabeln (som du använder som en "uppslagstabell" för entitet för att skriva mappningar) för att använda dessa nya typdefinitioner:
SchemaTable = #table({"Entity", "Type"}, {
{"Airlines", AirlinesType },
{"Airports", AirportsType },
{"People", PeopleType}
});
Framtvinga ett schema med hjälp av typer
Du förlitar dig på en vanlig funktion (Table.ChangeType
) för att framtvinga ett schema på dina data, ungefär som du använde SchemaTransformTable
i föregående lektion.
Till skillnad från SchemaTransformTable
tar Table.ChangeType
in en faktisk M-tabelltyp som argument och tillämpar schemat rekursivt för alla kapslade typer. Signaturen ser ut så här:
Table.ChangeType = (table, tableType as type) as nullable table => ...
Den fullständiga kodlistan för Table.ChangeType
funktionen finns i filen Table.ChangeType.pqm .
Kommentar
För flexibilitet kan funktionen användas i tabeller, samt listor med poster (vilket är hur tabeller skulle representeras i ett JSON-dokument).
Sedan måste du uppdatera anslutningskoden för att ändra parametern schema
från en table
till en type
och lägga till ett anrop Table.ChangeType
till 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
har uppdaterats för att använda listan över fält från schemat (för att känna till namnen på vad som ska expanderas när du får resultaten), men lämnar den faktiska schematillämpningen till 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äfta att kapslade typer anges
Definitionen för din PeopleType
nu anger fältet Emails
till en lista med text ({text}
).
Om du tillämpar typerna korrekt bör anropet till Type.ListItem i enhetstestet nu returneras type text
i stället type any
för .
När du kör enhetstesterna igen visas att alla nu skickas.
Omstrukturera vanlig kod till separata filer
Kommentar
M-motorn kommer att ha förbättrat stöd för att referera till externa moduler/vanlig kod i framtiden, men den här metoden bör föra dig igenom tills dess.
I det här läget har tillägget nästan lika mycket "vanlig" kod som TripPin-anslutningskod. I framtiden kommer dessa vanliga funktioner antingen att ingå i det inbyggda standardfunktionsbiblioteket, eller så kan du referera till dem från ett annat tillägg. För tillfället omstrukturerar du koden på följande sätt:
- Flytta de återanvändbara funktionerna till separata filer (.pqm).
- Ange egenskapen Build Action på filen till Kompilera för att se till att den ingår i filnamnstillägget under bygget.
- Definiera en funktion för att läsa in koden med expression.Evaluate.
- Läs in var och en av de vanliga funktioner som du vill använda.
Koden för att göra detta ingår i kodfragmentet nedan:
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");
Slutsats
Den här självstudien har gjort ett antal förbättringar av hur du framtvingar ett schema för de data som du får från ett REST-API. Anslutningsappen har för närvarande hård kodning av schemainformationen, som har en prestandaförmån vid körning, men som inte kan anpassas till ändringar i tjänstens metadataövertid. Framtida självstudier övergår till en rent dynamisk metod som härleder schemat från tjänstens $metadata dokument.
Förutom schemaändringarna lade den här självstudien till Enhetstester för koden och omstrukturerade de vanliga hjälpfunktionerna till separata filer för att förbättra den övergripande läsbarheten.