TripPin deel 7 - Geavanceerd schema met M-typen

Deze meerdelige zelfstudie gaat over het maken van een nieuwe gegevensbronextensie voor Power Query. De zelfstudie is bedoeld om opeenvolgend te worden uitgevoerd elke les is gebaseerd op de connector die in de vorige lessen is gemaakt, en voegt incrementeel nieuwe mogelijkheden toe — aan uw connector.

In deze les gaat u het volgende doen:

  • Een tabelschema afdwingen met M-typen
  • Typen instellen voor geneste records en lijsten
  • Code herfactoreren voor hergebruik en eenheidstests

In de vorige les hebt u uw tabelschema's gedefinieerd met behulp van een eenvoudig systeem 'Schematabel'. Deze schematabelbenadering werkt voor veel REST API's/gegevensconnectoren, maar services die volledige of diep geneste gegevenssets retourneren, kunnen profiteren van de aanpak in deze zelfstudie, waarbij gebruik wordt gemaakt van het M-typesysteem.

In deze les wordt u door de volgende stappen geleid:

  1. Eenheidstests toevoegen
  2. Aangepaste M-typen definiëren
  3. Een schema afdwingen met behulp van typen
  4. Algemene code herfactoring in afzonderlijke bestanden

Eenheidstests toevoegen

Voordat u gebruik gaat maken van de geavanceerde schemalogica, voegt u een set eenheidstests toe aan uw connector om de kans op onbedoelde fouten te verkleinen. Eenheidstests werken als het volgende:

  1. Kopieer de algemene code uit het UnitTest-voorbeeld naar uw TripPin.query.pq bestand
  2. Een sectiedeclaratie toevoegen aan de bovenkant van uw TripPin.query.pq bestand
  3. Een gedeelde record maken (met de naam TripPin.UnitTest )
  4. Een definiëren Fact voor elke test
  5. Facts.Summarize()Aanroepen om alle tests uit te voeren
  6. Verwijs naar de vorige aanroep als de gedeelde waarde om ervoor te zorgen dat deze wordt geëvalueerd wanneer het project wordt uitgevoerd in 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];

Als u op Uitvoeren op het project klikt, worden alle feiten geëvalueerd en krijgt u een rapportuitvoer die er als volgende uitziet:

Eerste eenheidstest.

Aan de hand van test-driven developmentgaat u nu een test toevoegen die momenteel mislukt, maar binnenkort opnieuw wordtimplementeerd en opgelost (aan het einde van deze zelfstudie). U voegt met name een test toe die een van de geneste records (e-mailberichten) controleert die u in de entiteit Personen krijgt.

Fact("Emails is properly typed", type text, Type.ListItem(Value.Type(People{0}[Emails])))

Als u de code opnieuw wilt uitvoeren, ziet u nu dat u een mislukte test hebt.

Eenheidstest met fout.

Nu hoeft u alleen nog de functionaliteit te implementeren om dit te laten werken.

Aangepaste M-typen definiëren

Bij de methode voor het afdwingen van schema's in de vorige les zijn 'schematabellen' gebruikt die zijn gedefinieerd als naam-/typeparen. Het werkt goed bij het werken met platte/relationele gegevens, maar biedt geen ondersteuning voor instellingstypen voor geneste records/tabellen/lijsten, of u kunt typedefinities hergebruiken in tabellen/entiteiten.

In het geval van TripPin bevatten de gegevens in de entiteiten People en Airports gestructureerde kolommen en delen ze zelfs een type ( ) voor Location het vertegenwoordigen van adresgegevens. In plaats van naam-/typeparen in een schematabel te definiëren, definieert u elk van deze entiteiten met behulp van aangepaste M-typedeclaraties.

Hier volgen een korte informatievernieuwing over typen in de M-taal uit de taalspecificatie:

Een typewaarde is een waarde die andere waarden classificeert. Van een waarde die wordt geclassificeerd door een type wordt gezegd dat deze voldoet aan dat type. Het M-typesysteem bestaat uit de volgende soorten typen:

  • Primitieve typen, die primitieve waarden classificeren (binary, date, datetime, datetimezone, duration, list, logical, null, number, record, text, time, type) en ook een aantal abstracte typen bevatten (function, table, anyen none)
  • Recordtypen, die recordwaarden classificeren op basis van veldnamen en waardetypen
  • Lijsttypen, waarmee lijsten worden geclassificeerd met behulp van één itembasistype
  • Functietypen, waarmee functiewaarden worden geclassificeerd op basis van de typen van de parameters en retourwaarden
  • Tabeltypen, waarmee tabelwaarden worden geclassificeerd op basis van kolomnamen, kolomtypen en sleutels
  • Typen waarvoor null is toegestaan, waarmee de waarde null wordt geclassificeerd naast alle waarden die zijn geclassificeerd door een basistype
  • Typetypen, waarmee waarden worden geclassificeerd die typen zijn

Met behulp van de onbewerkte JSON-uitvoer die u krijgt (en/of de definities opzoekt in de $metadata van de service), kunt u de volgende recordtypen definiëren om complexe OData-typen weer te geven:

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
];

Let op hoe de LocationType verwijst naar CityType de en om de gestructureerde kolommen weer te LocType geven.

Voor entiteiten op het hoogste niveau (die u wilt vertegenwoordigen als Tabellen), definieert u tabeltypen:

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
];

Vervolgens werk u de variabele (die u als opzoektabel gebruikt voor entiteitstoewijzingen) bij om deze nieuwe typedefinities SchemaTable te gebruiken:

SchemaTable = #table({"Entity", "Type"}, {
    {"Airlines", AirlinesType },    
    {"Airports", AirportsType },
    {"People", PeopleType}    
});

Een schema afdwingen met behulp van typen

U vertrouwt op een algemene functie ( ) om een schema af te dwingen voor uw gegevens, net zoals u Table.ChangeType in de vorige les hebt SchemaTransformTable gebruikt. In tegenstelling tot wordt een daadwerkelijk M-tabeltype als argument gebruikt en wordt uw SchemaTransformTable Table.ChangeType schema recursief toegepast voor alle geneste typen. De handtekening ziet er als volgende uit:

Table.ChangeType = (table, tableType as type) as nullable table => ...

De volledige codevermelding voor Table.ChangeType de functie vindt u in het bestand Table.ChangeType.pqm.

Notitie

Voor flexibiliteit kan de functie worden gebruikt in tabellen, evenals lijsten met records (zoals tabellen worden weergegeven in een JSON-document).

Vervolgens moet u de connectorcode bijwerken om de parameter te wijzigen van een in een en een schema table type aanroep toevoegen Table.ChangeType aan in 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 is bijgewerkt om de lijst met velden uit het schema te gebruiken (om de namen te weten van wat moet worden uitgebreid wanneer u de resultaten krijgt), maar blijft het afdwingen van het daadwerkelijke schema op 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];

Bevestigen dat geneste typen worden ingesteld

De definitie voor uw PeopleType stelt het veld nu in op een lijst met tekst ( Emails {text} ). Als u de typen correct wilt toepassen, zou de aanroep van Type.ListItem in uw eenheidstest nu moeten worden retourneert in type text plaats van type any .

Als u de eenheidstests opnieuw wilt uitvoeren, kunt u zien dat ze nu allemaal worden doorstaan.

Eenheidstest met succes.

Algemene code herfactoring in afzonderlijke bestanden

Notitie

De M-engine heeft in de toekomst verbeterde ondersteuning voor het verwijzen naar externe modules/algemene code, maar deze aanpak moet u tot dan toe doorverdienen.

Op dit moment heeft uw extensie bijna net zoveel 'algemene' code als de code van de TripPin-connector. In de toekomst zullen deze algemene functies deel uitmaken van de ingebouwde standaard functiebibliotheek, of u kunt hier vanuit een andere extensie naar verwijzen. Op dit moment herfactort u uw code op de volgende manier:

  1. Verplaats de herbruikbare functies naar afzonderlijke bestanden (.pqm).
  2. Stel de eigenschap Build Action van het bestand in op Compileren om ervoor te zorgen dat deze tijdens de build wordt opgenomen in het extensiebestand.
  3. Definieer een functie om de code te laden met expression.Evaluate.
  4. Laad elk van de algemene functies die u wilt gebruiken.

De code om dit te doen, is opgenomen in het onderstaande fragment:

Extension.LoadFunction = (name as text) =>
    let
        binary = Extension.Contents(name),
        asText = Text.FromBinary(binary)
    in
        Expression.Evaluate(asText, #shared);

Table.ChangeType = Extension.LoadFunction("Table.ChangeType.pqm");
Table.GenerateByPage = Extension.LoadFunction("Table.GenerateByPage.pqm");
Table.ToNavigationTable = Extension.LoadFunction("Table.ToNavigationTable.pqm");

Conclusie

In deze zelfstudie zijn een aantal verbeteringen aangebracht in de manier waarop u een schema afdwingt voor de gegevens die u van een REST API. De connector is momenteel hard bezig met het coderen van de schemagegevens. Dit heeft een prestatievoordeel tijdens runtime, maar kan zich niet aanpassen aan wijzigingen in de metagegevens-overtime van de service. Toekomstige zelfstudies gaan over op een uitsluitend dynamische benadering die het schema uit het document van de service $metadata afleiden.

Naast de schemawijzigingen zijn in deze zelfstudie eenheidstests voor uw code toegevoegd en zijn de algemene helperfuncties ge herfactoreerd in afzonderlijke bestanden om de algehele leesbaarheid te verbeteren.

Volgende stappen

TripPin deel 8: Diagnostische gegevens toevoegen