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:
- Dodawanie testów jednostkowych.
- Definiowanie niestandardowych typów języka M.
- Wymuszanie schematu przy użyciu typów.
- 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:
- Skopiuj wspólny kod z przykładu UnitTest do pliku
TripPin.query.pq
. - Dodaj deklarację sekcji na początku
TripPin.query.pq
pliku. - Utwórz udostępniony rekord (o nazwie
TripPin.UnitTest
). - Zdefiniuj element
Fact
dla każdego testu. - Wywołaj metodę
Facts.Summarize()
, aby uruchomić wszystkie testy. - 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:
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.
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
,list
logical
datetimezone
type
duration
time
null
number
record
datetime
text
), a także zawierają wiele typów abstrakcyjnych (function
,table
,any
i )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 SchemaTransformTable
klasy , 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 GetEntity
wartość .
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.
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:
- Przenieś funkcje wielokrotnego użytku do oddzielnych plików (pqm).
- Ustaw właściwość Akcja kompilacji w pliku na skompiluj, aby upewnić się, że zostanie ona uwzględniona w pliku rozszerzenia podczas kompilacji.
- Zdefiniuj funkcję, aby załadować kod przy użyciu funkcji Expression.Evaluate.
- 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ść.