Udostępnij za pośrednictwem


TripPin — część 7 — zaawansowany schemat z typami M

Uwaga

Ta zawartość obecnie odwołuje się do zawartości ze starszej implementacji na potrzeby testowania jednostkowego w programie Visual Studio. Zawartość zostanie zaktualizowana w najbliższej przyszłości, aby uwzględnić nową strukturę testową zestawu POWER Query SDK.

Ten wieloczęściowy samouczek obejmuje tworzenie nowego rozszerzenia źródła danych dla dodatku Power Query. Samouczek ma być wykonywany sekwencyjnie — każda lekcja opiera się na łączniku utworzonym w poprzednich lekcjach, przyrostowo dodając nowe możliwości do łącznika.

W tej lekcji wykonasz następujące lekcji:

  • Wymuszanie schematu tabeli przy użyciu typów M
  • Ustawianie typów dla zagnieżdżonych rekordów i list
  • Refaktoryzacja kodu do ponownego użycia i testowania jednostkowego

W poprzedniej lekcji zdefiniowano schematy tabel przy użyciu prostego systemu "Tabela schematów". To podejście do tabeli schematu działa w przypadku wielu interfejsów API REST/Połączenie or danych, ale usługi, które zwracają kompletne lub głęboko zagnieżdżone zestawy danych, mogą skorzystać z podejścia w tym samouczku, które korzysta z systemu typów M.

Ta lekcja przeprowadzi Cię przez następujące kroki:

  1. Dodawanie testów jednostkowych.
  2. Definiowanie niestandardowych typów języka M.
  3. Wymuszanie schematu przy użyciu typów.
  4. Refaktoryzacja wspólnego kodu w osobnych plikach.

Dodawanie testów jednostkowych

Przed rozpoczęciem korzystania z zaawansowanej logiki schematu dodasz zestaw testów jednostkowych do łącznika, aby zmniejszyć prawdopodobieństwo przypadkowego przerwania czegoś. Testowanie jednostkowe działa w następujący sposób:

  1. Skopiuj wspólny kod z przykładu UnitTest do plikuTripPin.query.pq.
  2. Dodaj deklarację sekcji na początku TripPin.query.pq pliku.
  3. Utwórz udostępniony rekord (o nazwie TripPin.UnitTest).
  4. Zdefiniuj element Fact dla każdego testu.
  5. Wywołaj metodę Facts.Summarize() , aby uruchomić wszystkie testy.
  6. Odwołaj się do poprzedniego wywołania jako wartości udostępnionej, aby upewnić się, że jest obliczana po uruchomieniu projektu w programie 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];

Wybranie opcji Uruchom w projekcie spowoduje ocenę wszystkich faktów i przekazanie danych wyjściowych raportu, które wyglądają następująco:

Początkowy test jednostkowy.

Korzystając z niektórych zasad z zakresu programowania opartego na testach, dodasz teraz test, który obecnie kończy się niepowodzeniem, ale wkrótce zostanie ponownie zaimplementowany i naprawiony (do końca tego samouczka). W szczególności dodasz test sprawdzający jeden z zagnieżdżonych rekordów (wiadomości e-mail), które zostaną zwrócone w jednostce Osoby.

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

Jeśli ponownie uruchomisz kod, powinien zostać wyświetlony test zakończony niepowodzeniem.

Test jednostkowy z niepowodzeniem.

Teraz wystarczy zaimplementować funkcje, aby wykonać tę pracę.

Definiowanie niestandardowych typów języka M

Podejście wymuszania schematu w poprzedniej lekcji używało "tabel schematu" zdefiniowanych jako pary nazw/typów. Działa dobrze podczas pracy z spłaszczone/relacyjnymi danymi, ale nie obsługuje typów ustawień w zagnieżdżonych rekordach/tabelach/listach lub umożliwia ponowne używanie definicji typów między tabelami/jednostkami.

W przypadku TripPin dane w jednostkach Osoby i Lotniska zawierają kolumny ustrukturyzowane, a nawet współużytkują typ (Location) do reprezentowania informacji o adresie. Zamiast definiować pary nazwa/typ w tabeli schematu, zdefiniujesz każdą z tych jednostek przy użyciu niestandardowych deklaracji typów języka M.

Poniżej przedstawiono szybsze odświeżanie typów w języku M ze specyfikacji języka:

Wartość typu to wartość, która klasyfikuje inne wartości. Wartość sklasyfikowana przez typ jest określana jako zgodna z tym typem. System typów M składa się z następujących rodzajów typów:

  • Typy pierwotne, które klasyfikują wartości pierwotne (binary, date, listlogicaldatetimezonetypedurationtimenullnumberrecorddatetimetext), a także zawierają wiele typów abstrakcyjnych (function, table, anyi )none
  • Typy rekordów, które klasyfikują wartości rekordów na podstawie nazw pól i typów wartości
  • Typy list, które klasyfikują listy przy użyciu typu podstawowego pojedynczego elementu
  • Typy funkcji, które klasyfikują wartości funkcji na podstawie typów ich parametrów i zwracanych wartości
  • Typy tabel, które klasyfikują wartości tabeli na podstawie nazw kolumn, typów kolumn i kluczy
  • Typy dopuszczane wartości null, które klasyfikuje wartość null oprócz wszystkich wartości sklasyfikowanych przez typ podstawowy
  • Typy typów, które klasyfikują wartości, które są typami

Korzystając z nieprzetworzonych danych wyjściowych JSON (i/lub wyszukując definicje w $metadata usługi), można zdefiniować następujące typy rekordów do reprezentowania typów złożonych OData:

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

Zwróć uwagę, LocationType jak odwołania do CityType elementu i LocType reprezentują kolumny ustrukturyzowane.

W przypadku jednostek najwyższego poziomu (które mają być reprezentowane jako tabele), należy zdefiniować typy tabel:

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

Następnie zaktualizujesz SchemaTable zmienną (która będzie używana jako "tabela odnośników" dla jednostki do mapowania typów), aby użyć tych nowych definicji typów:

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

Wymuszanie schematu przy użyciu typów

Będziesz polegać na typowej funkcji (Table.ChangeType), aby wymusić schemat danych, podobnie jak w SchemaTransformTable poprzedniej lekcji. W przeciwieństwie do SchemaTransformTableklasy , Table.ChangeType przyjmuje rzeczywisty typ tabeli języka M jako argument i stosuje schemat rekursywnie dla wszystkich zagnieżdżonych typów. Jego podpis wygląda następująco:

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

Pełną listę Table.ChangeType kodu funkcji można znaleźć w pliku Table.ChangeType.pqm .

Uwaga

W celu zapewnienia elastyczności funkcja może być używana w tabelach, a także na listach rekordów (w jaki sposób tabele będą reprezentowane w dokumencie JSON).

Następnie należy zaktualizować kod łącznika, aby zmienić schema parametr z na table type, i dodać wywołanie metody Table.ChangeType w pliku .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 Program jest aktualizowany w celu używania listy pól ze schematu (aby poznać nazwy elementów, które należy rozwinąć po korzystaniu z wyników), ale powoduje pozostawienie rzeczywistego wymuszania schematu na GetEntitywartość .

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

Potwierdzanie, że typy zagnieżdżone są ustawiane

Definicja teraz PeopleType ustawia Emails pole na listę tekstu ({text}). Jeśli poprawnie stosujesz typy, wywołanie metody Type.ListItem w teście jednostkowym powinno teraz zwracać type text wartość zamiast type any.

Ponowne uruchomienie testów jednostkowych pokazuje, że wszystkie te testy są teraz pomijane.

Test jednostkowy z powodzeniem.

Refaktoryzacja wspólnego kodu w osobnych plikach

Uwaga

Aparat języka M będzie miał ulepszoną obsługę odwoływania się do modułów zewnętrznych/wspólnego kodu w przyszłości, ale takie podejście powinno być kontynuowane do tego czasu.

W tym momencie rozszerzenie ma prawie tyle "wspólnego" kodu, jak kod łącznika TripPin. W przyszłości te typowe funkcje będą częścią wbudowanej standardowej biblioteki funkcji lub będzie można odwoływać się do nich z innego rozszerzenia. Na razie refaktoryzujesz kod w następujący sposób:

  1. Przenieś funkcje wielokrotnego użytku do oddzielnych plików (pqm).
  2. Ustaw właściwość Akcja kompilacji w pliku na skompiluj, aby upewnić się, że zostanie ona uwzględniona w pliku rozszerzenia podczas kompilacji.
  3. Zdefiniuj funkcję, aby załadować kod przy użyciu funkcji Expression.Evaluate.
  4. Załaduj każdą z typowych funkcji, których chcesz użyć.

Kod, który ma to zrobić, znajduje się w poniższym fragmencie kodu:

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");

Podsumowanie

W tym samouczku wprowadzono szereg ulepszeń sposobu wymuszania schematu na danych pobieranych z interfejsu API REST. Łącznik jest obecnie trwale kodujący informacje o schemacie, które mają korzyść z wydajności w czasie wykonywania, ale nie jest w stanie dostosować się do zmian w nadgodzinach metadanych usługi. Przyszłe samouczki przejdą do czysto dynamicznego podejścia, które będzie wnioskować o schemacie z dokumentu $metadata usługi.

Oprócz zmian schematu ten samouczek dodał testy jednostkowe dla kodu i refaktoryzował typowe funkcje pomocnicze w osobnych plikach, aby zwiększyć ogólną czytelność.

Następne kroki

TripPin — część 8 — dodawanie diagnostyki