Поделиться через


Часть 7 TripPin — расширенная схема с типами M

Примечание.

В настоящее время это содержимое ссылается на содержимое из устаревшей реализации для модульного тестирования в Visual Studio. Содержимое будет обновлено в ближайшее время, чтобы покрыть новую платформу тестового пакета SDK Power Query.

В этом руководстве рассматривается создание нового расширения источника данных для Power Query. Это руководство предназначено для последовательного выполнения каждого урока— каждый урок, созданный на основе соединителя, созданного на предыдущих уроках, постепенно добавляя новые возможности в соединитель.

В этом уроке вы научитесь:

  • Принудительное применение схемы таблицы с помощью типов M
  • Установка типов для вложенных записей и списков
  • Рефакторинг кода для повторного использования и модульного тестирования

На предыдущем занятии вы определили схемы таблиц с помощью простой системы "Таблица схемы". Этот подход к таблице схем подходит для многих интерфейсов REST API/data Подключение or, но службы, возвращающие полные или глубоко вложенные наборы данных, могут воспользоваться подходом в этом руководстве, который использует систему типов M.

На этом занятии вы узнаете, как выполнить следующие действия.

  1. Добавление модульных тестов.
  2. Определение пользовательских типов M.
  3. Применение схемы с помощью типов.
  4. Рефакторинг общего кода в отдельные файлы.

Добавление модульных тестов

Прежде чем приступить к использованию расширенной логики схемы, вы добавите набор модульных тестов в соединитель, чтобы уменьшить вероятность непреднамеренного нарушения чего-либо. Модульное тестирование работает следующим образом:

  1. Скопируйте общий код из примера UnitTest в TripPin.query.pq файл.
  2. Добавьте объявление раздела в начало TripPin.query.pq файла.
  3. Создайте общую запись (вызываетсяTripPin.UnitTest).
  4. Определите Fact для каждого теста.
  5. Вызов Facts.Summarize() для выполнения всех тестов.
  6. Обратитесь к предыдущему вызову в качестве общего значения, чтобы убедиться, что он вычисляется при запуске проекта в 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];

При выборе запуска в проекте будут оцениваться все факты и выводятся выходные данные отчета, которые выглядят следующим образом:

Начальный модульный тест.

Используя некоторые принципы разработки на основе тестов, вы добавите тест, который в настоящее время завершается сбоем, но скоро будет повторно завершен и исправлен (к концу этого руководства). В частности, вы добавите тест, который проверка одной из вложенных записей (emails), которые вы вернетесь в сущность Люди.

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

Если вы снова запустите код, вы увидите, что у вас произошел сбой теста.

Модульный тест с ошибкой.

Теперь вам просто нужно реализовать функциональные возможности, чтобы сделать эту работу.

Определение пользовательских типов M

Подход к применению схемы в предыдущем занятии использовал таблицы схемы, определенные как пары Name/Type. Он хорошо работает при работе с неструктурированными или реляционными данными, но не поддерживает типы параметров для вложенных записей, таблиц и списков или позволяют повторно использовать определения типов в таблицах или сущностях.

В случае TripPin данные в сущностях Люди и аэропортов содержат структурированные столбцы и даже совместно используют тип (Location) для представления сведений об адресе. Вместо определения пар name/Type в таблице схемы вы определите каждую из этих сущностей с помощью настраиваемых объявлений типов M.

Ниже приведен краткий обзор типов языка M из спецификации языка:

Значение типа — это значение, которое классифицирует другие значения. Значение, классифицируемые типом, как сообщается , соответствует такому типу. Система типов M состоит из следующих типов:

  • Примитивные типы, которые классифицируют примитивные значения (binary, nulldurationdatedatetimezonetypelisttimenumberdatetimetextlogicalrecord) и включают ряд абстрактных типов (function, table, anyи)none
  • Типы записей, которые классифицируют значения записей на основе имен полей и типов значений
  • Типы списков, которые классифицируют списки с помощью базового типа одного элемента
  • Типы функций, которые классифицируют значения функций на основе типов их параметров и возвращаемых значений
  • Типы таблиц, которые классифицируют значения таблиц на основе имен столбцов, типов столбцов и ключей
  • Типы, допускающие значение NULL, классифицируют значение NULL в дополнение ко всем значениям, классифицированным базовым типом.
  • Типы типов, которые классифицируют значения, которые являются типами

Используя необработанные выходные данные JSON (и(или) поиск определений в $metadata службы, можно определить следующие типы записей для представления сложных типов 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
];

Обратите внимание, LocationType как ссылки на CityType столбцы и LocType представлять структурированные столбцы.

Для сущностей верхнего уровня (которые вы хотите представить в виде таблиц), определите типы таблиц:

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

Затем вы обновите SchemaTable переменную (которая используется в качестве таблицы подстановки для сопоставления сущностей для типов) для использования этих новых определений типов:

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

Применение схемы с помощью типов

Вы будете полагаться на общую функцию (Table.ChangeType) для принудительного применения схемы к данным, как и в SchemaTransformTableпредыдущем занятии. В отличие SchemaTransformTableот того, Table.ChangeType принимает фактический тип таблицы M в качестве аргумента и будет применять схему рекурсивно для всех вложенных типов. Его подпись выглядит следующим образом:

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

Полный список кода для Table.ChangeType функции можно найти в файле Table.ChangeType.pqm .

Примечание.

Для гибкости функцию можно использовать в таблицах, а также списки записей (как таблицы будут представлены в документе JSON).

Затем необходимо обновить код соединителя, чтобы изменить schema параметр с a table на type,и добавить вызов Table.ChangeType в 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 обновляется, чтобы использовать список полей из схемы (чтобы узнать имена, которые необходимо развернуть при получении результатов), но оставляет фактическое применение 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];

Подтверждение установки вложенных типов

Определение для PeopleType поля теперь задает Emails список текста ({text}). Если вы правильно применяете типы, вызов Type.ListItem в модульном тесте теперь должен возвращаться type text , а не type any.

При выполнении модульных тестов снова показано, что они теперь передаются.

Модульный тест с успехом.

Рефакторинг общего кода в отдельные файлы

Примечание.

Модуль M будет иметь улучшенную поддержку ссылок на внешние модули или общий код в будущем, но этот подход должен провести вас до тех пор.

На этом этапе расширение почти имеет такую общую коду, как код соединителя TripPin. В будущем эти общие функции будут либо частью встроенной стандартной библиотеки функций , либо вы сможете ссылаться на них из другого расширения. Теперь вы рефакторингируйте код следующим образом:

  1. Переместите повторно используемые функции в отдельные файлы (PQM).
  2. Задайте свойству Действия сборки файл, чтобы скомпилировать его, чтобы убедиться, что он включен в файл расширения во время сборки.
  3. Определите функцию для загрузки кода с помощью Expression.Evaluate.
  4. Загрузите каждую из распространенных функций, которые вы хотите использовать.

Этот код включается в фрагмент ниже:

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

Заключение

В этом руководстве показано, как применить схему к данным, полученным из REST API. Соединитель в настоящее время жестко кодирует сведения о схеме, что имеет преимущество производительности во время выполнения, но не может адаптироваться к изменениям в метаданных службы сверхурочно. Будущие учебники будут переходить к чисто динамическому подходу, который будет выводить схему из $metadata документа службы.

Помимо изменений схемы, в этом руководстве добавлены модульные тесты для кода и рефакторинг общих вспомогательных функций в отдельные файлы для улучшения общей удобочитаемости.

Следующие шаги

Часть 8 TripPin — добавление диагностики