Dela via


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:

  1. Lägga till enhetstester.
  2. Definiera anpassade M-typer.
  3. Framtvinga ett schema med hjälp av typer.
  4. 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:

  1. Kopiera den vanliga koden från UnitTest-exemplet till filen TripPin.query.pq .
  2. Lägg till en avsnittsdeklaration överst TripPin.query.pq i filen.
  3. Skapa en delad post (kallas TripPin.UnitTest).
  4. Definiera en Fact för varje test.
  5. Anropa Facts.Summarize() för att köra alla tester.
  6. 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:

Inledande enhetstest.

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.

Enhetstest med fel.

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, , datetimedate, datetimezone, duration, list, logical, null, number, record, text, , time) typeoch även innehåller ett antal abstrakta typer (function, table, anyoch none)
  • 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 SchemaTransformTabletar 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 typeoch 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 anyför .

När du kör enhetstesterna igen visas att alla nu skickas.

Enhetstest med framgång.

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:

  1. Flytta de återanvändbara funktionerna till separata filer (.pqm).
  2. Ange egenskapen Build Action på filen till Kompilera för att se till att den ingår i filnamnstillägget under bygget.
  3. Definiera en funktion för att läsa in koden med expression.Evaluate.
  4. 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.

Nästa steg

TripPin Del 8 – Lägga till diagnostik