Udostępnij przez


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

Uwaga / Notatka

Ta zawartość obecnie odwołuje się do zawartości ze starszej implementacji na potrzeby testowania jednostkowego w programie Visual Studio. Zawartość zostanie zaktualizowana w 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:

  • Wymuszanie schematu tabeli przy użyciu typów M
  • Ustawianie typów dla zagnieżdżonych rekordów i list
  • Refaktoryzacja kodu w celu 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/łączników danych. Jednak usługi, które zwracają kompletne lub głęboko zagnieżdżone zestawy danych, mogą korzystać z podejścia w tym samouczku, które wykorzystuje system 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 należy dodać 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 pliku TripPin.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 zapewnić jej obliczenie, gdy projekt jest uruchamiany 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];

Uruchomienie projektu spowoduje ocenę wszystkich Faktów i wygeneruje raport, którego dane wyjściowe wyglądają następująco:

Zrzut ekranu przedstawiający kartę Dane wyjściowe zapytania języka M z wyświetlonymi pomyślnymi danymi wyjściowymi raportu.

Korzystając z niektórych zasad z zakresu programowania opartego na testach, teraz dodasz test, który obecnie kończy się niepowodzeniem, ale wkrótce można go ponownie zaimplementować i naprawić (na końcu tego samouczka). W szczególności dodasz test sprawdzający jeden z zagnieżdżonych rekordów (E-maili), które otrzymujesz z jednostki People.

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.

Zrzut ekranu karty Wynik zapytania języka M pokazujący testy, które zakończyły się 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łaszczanymi/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 People and Airports zawierają kolumny ustrukturyzowane, a nawet współużytkują typ (Location) do reprezentowania informacji o adresie. Zamiast definiować pary nazwa/typ w tabeli schematu, należy zdefiniować każdą z tych jednostek przy użyciu niestandardowych deklaracji typów języka M.

Oto krótkie przypomnienie typów w języku M z 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, datetime, datetimezone, duration, list, logical, null, number, record, text, time, type), a także zawierają niewielką liczbę typów abstrakcyjnych (function, table, any, 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 dopuszczające wartości null, które umożliwiają klasyfikację wartości null poza wszystkimi wartościami sklasyfikowanymi 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 odnosi się do CityType i LocType aby odzwierciedlać ustrukturyzowane kolumny.

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

Używasz typowej funkcji (Table.ChangeType), aby wymusić schemat na danych, podobnie jak w SchemaTransformTablepoprzedniej lekcji. W przeciwieństwie do SchemaTransformTable, Table.ChangeType przyjmuje rzeczywisty typ tabeli 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łny listing kodu funkcji Table.ChangeType można znaleźć w pliku Table.ChangeType.pqm.

Uwaga / Notatka

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ć parametr schema z table na type, i dodać wywołanie Table.ChangeType w 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;

Aktualizowane jest użycie listy pól ze schematu (aby poznać nazwy elementów do rozwinęcia podczas otrzymywania wyników), ale rzeczywiste wymuszanie schematu pozostawia się 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];

Potwierdzanie, czy typy zagnieżdżone są poprawnie ustawiane

Definicja dla twojego PeopleType teraz ustawia pole Emails 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 teraz wszystkie przechodzą.

Zrzut ekranu karty Dane wyjściowe zapytania języka M pokazujący pomyślne przeprowadzenie testów jednostkowych.

Refaktoryzacja wspólnego kodu w osobnych plikach

Uwaga / Notatka

Silnik M będzie miał lepsze wsparcie dla odwoływania się do modułów zewnętrznych/wspólnego kodu w przyszłości, ale to podejście powinno wystarczyć 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 do wykonania tej refaktoryzacji jest uwzględniony w następującym 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 w pełni dynamicznego podejścia, które będzie wnioskowało schemat 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ść.

Dalsze kroki

TripPin — część 8 — dodawanie diagnostyki