Compartilhar via


TripPin parte 7 - Esquema avançado com tipos M

Observação

Esse conteúdo atualmente faz referência ao conteúdo de uma implementação antiga para teste de unidade no Visual Studio. O conteúdo será atualizado em um futuro próximo para abranger a nova estrutura de teste do SDK do Power Query.

Este tutorial de várias partes aborda a criação de uma nova extensão de fonte de dados para o Power Query. O tutorial deve ser seguido sequencialmente; cada lição se baseia no conector criado nas lições anteriores, adicionando incrementalmente novos recursos a ele.

Nesta lição, você vai:

  • Impor um esquema de tabela usando tipos M
  • Definir tipos para listas e registros aninhados
  • Refatorar códigos para reutilização e teste de unidade

Na lição anterior, você definiu seus esquemas de tabela usando um simples sistema de "Tabela de Esquema". Essa abordagem de tabela de esquema funciona para muitas APIs REST/conectores de dados, mas os serviços que retornam conjuntos de dados completos ou profundamente aninhados podem se beneficiar da abordagem neste tutorial, que utiliza o sistema de tipos M.

Esta lição abordará as seguintes etapas:

  1. Adicionar testes de unidade.
  2. Definir tipos M personalizados.
  3. Impor um esquema usando tipos.
  4. Refatorar códigos comuns em arquivos separados.

Adicionar testes de unidade

Antes de começar a usar a lógica de esquema avançada, você adicionará um conjunto de testes de unidade ao conector para reduzir a chance de alguma interrupção inadvertida. O teste de unidade funciona da seguinte maneira:

  1. Copie o código comum do exemplo UnitTest para o seu TripPin.query.pq arquivo.
  2. Adicione uma declaração de seção no topo do seu TripPin.query.pq arquivo.
  3. Crie um registro compartilhado (chamado TripPin.UnitTest).
  4. Defina um Fact teste para cada um.
  5. Chame Facts.Summarize() para executar todos os testes.
  6. Referencie a chamada anterior como valor compartilhado, garantindo que seja avaliado quando o projeto for executado no 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];

Ao selecionar "executar" no projeto, avaliará todos os Fatos e fornecerá um relatório de saída que se parece com este:

Teste de unidade inicial.

Usando alguns princípios do desenvolvimento controlado por teste, agora você adicionará um teste que falhará, mas que será reimplementado e corrigido em breve (até o final deste tutorial). Especificamente, você adicionará um teste que verificará um dos registros aninhados (emails) que você obtém na entidade Pessoas.

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

Se você executar o código novamente, verá que há um teste com falha.

Teste de unidade com falha.

Agora você só precisa implementar a funcionalidade para que isso funcione.

Definir tipos M personalizados

A abordagem de imposição de esquema na lição anterior usou "tabelas de esquema" definidas como pares Nome/Tipo. Ela funciona bem ao trabalhar com dados achatados/relacionais, mas não dá suporte a tipos de configuração em registros/tabelas/listas aninhados nem permite reutilizar definições de tipo entre tabelas/entidades.

No caso do TripPin, os dados nas entidades Pessoas e Aeroportos contêm colunas estruturadas e até compartilham um tipo (Location) para representar informações de endereço. Em vez de definir pares nome/tipo em uma tabela de esquema, você definirá cada uma dessas entidades usando declarações de tipo M personalizadas.

Veja a seguinte atualização rápida sobre os tipos na linguagem M na Especificação de linguagem:

Um valor de tipo é um valor que classifica outros valores. Um valor classificado por um tipo obedece a esse tipo. O sistema de tipos de M é composto pelas seguintes categorias de tipos:

  • Tipos primitivos, que classificam valores primitivos (binary, date, datetime, datetimezone, duration, list, logical, null, number, record, text, time, type) e também incluem vários tipos abstratos (function, table, any, none)
  • Tipos de registro, que classificam valores de registro com base em nomes de campo e em tipos de valor
  • Tipos de lista, que classificam listas usando apenas um tipo de base de item
  • Tipos de função, que classificam valores de função com base nos tipos de seus parâmetros e valores de retorno
  • Tipos de tabela, que classificam valores de tabela com base em nomes de coluna, tipos de coluna e chaves
  • Tipos anuláveis, que classificam o valor nulo além de todos os valores classificados por um tipo base
  • Tipos de tipo, que classificam valores que são tipos

Usando a saída JSON bruta que você recebe (e/ou pesquisando as definições nos $metadata do serviço), é possível definir os seguintes tipos de registro para representar tipos OData complexos:

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

Observe como LocationType faz referência a CityType e a LocType para representar suas colunas estruturadas.

Para as entidades de nível superior que você deseja representar como tabelas, é possível definir tipos de tabela:

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

Em seguida, é possível atualizar sua variável SchemaTable (que você usará como uma “tabela de pesquisa” para mapeamentos de entidade para tipo) a fim de usar essas novas definições de tipo:

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

Impor um esquema usando tipos

É possível contar com uma função comum (Table.ChangeType) para impor um esquema aos dados, assim como você fez usando SchemaTransformTable no exercício anterior. Ao contrário de SchemaTransformTable, Table.ChangeType usa um tipo de tabela M real como um argumento e aplica seu esquema recursivamente a todos os tipos aninhados. Sua assinatura tem esta aparência:

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

A listagem de código completa para a função Table.ChangeType pode ser encontrada no arquivo Table.ChangeType.pqm.

Observação

Para obter flexibilidade, a função pode ser usada em tabelas e em listas de registros (que é como as tabelas são representadas em um documento JSON).

Em seguida, você precisará atualizar o código do conector para alterar o parâmetro schema de um table para um type e adicionar uma chamada a Table.ChangeType em 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 é atualizado para usar a lista de campos do esquema (para saber os nomes do que expandir quando você obtém os resultados), mas deixa a imposição real do esquema para 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];

Confirmar que tipos aninhados estão sendo definidos

A definição de PeopleType agora define o campo Emails como uma lista de texto ({text}). Se você estiver aplicando os tipos corretamente, a chamada para Type.ListItem no teste de unidade agora deverá retornar type text em vez de type any.

Executar os testes de unidade novamente mostra que eles agora estão todos sendo aprovados.

Teste de unidade com êxito.

Refatorar códigos comuns em arquivos separados

Observação

O mecanismo M terá suporte aprimorado para referenciar módulos externos/códigos comuns no futuro, mas essa abordagem deve servir até lá.

Neste ponto, sua extensão tem quase tanto código "comum" quanto o código do conector TripPin. No futuro, essas funções comuns farão parte da biblioteca de funções padrão interna ou será possível referenciá-las de outra extensão. Por enquanto, você refatora seu código da seguinte maneira:

  1. Mova as funções reutilizáveis para arquivos separados (.pqm).
  2. Defina a propriedade Ação de build no arquivo para Compilar a fim de garantir que ela seja incluída no arquivo de extensão durante o build.
  3. Defina uma função para carregar o código usando Expression.Evaluate.
  4. Carregue cada uma das funções comuns que você deseja usar.

O código para isso está incluído no snippet abaixo:

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

Conclusão

Este tutorial fez uma série de melhorias na maneira como você impõe um esquema nos dados obtidos de uma API REST. No momento, o conector está codificando as informações do esquema, que têm um benefício de desempenho no runtime, mas não consegue se adaptar às alterações nos metadados do serviço ao longo do tempo. Tutoriais futuros passarão para uma abordagem puramente dinâmica que inferirá o esquema do documento $metadata do serviço.

Além das alterações de esquema, este tutorial adicionou testes de unidade para seu código e refatorou as funções auxiliares comuns em arquivos separados a fim de melhorar a legibilidade geral.

Próximas etapas

TripPin Part 8 – Adicionar diagnóstico