Partilhar via


TripPin parte 7 - Esquema avançado com tipos M

Nota

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

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

Nesta lição, você irá:

  • Impor um esquema de tabela usando M Types
  • Definir tipos para registros e listas aninhados
  • Código de refatoração para reutilização e teste unitário

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

Esta lição irá guiá-lo através das seguintes etapas:

  1. Adicionando testes de unidade.
  2. Definição de tipos M personalizados.
  3. Impondo um esquema usando tipos.
  4. Refatoração de código comum em arquivos separados.

Adicionando 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 seu conector para reduzir a chance de quebrar algo inadvertidamente. O teste de unidade funciona assim:

  1. Copie o código comum do exemplo UnitTest para o TripPin.query.pq arquivo.
  2. Adicione uma declaração de secção à parte superior do ficheiro TripPin.query.pq .
  3. Crie um registro compartilhado (chamado TripPin.UnitTest).
  4. Defina um Fact para cada teste.
  5. Chamada Facts.Summarize() para executar todos os testes.
  6. Faça referência à chamada anterior como o valor compartilhado para garantir que ele 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];

Selecionar executar no projeto avaliará todos os fatos e fornecerá uma saída de relatório semelhante a esta:

Teste Unitário Inicial.

Usando alguns princípios do desenvolvimento orientado a testes, você adicionará um teste que atualmente falha, mas em breve será reimplementado e corrigido (até o final deste tutorial). Especificamente, você adicionará um teste que verifica um dos registros aninhados (E-mails) que você recebe de volta na entidade Pessoas.

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

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

Teste unitário com falha.

Agora você só precisa implementar a funcionalidade para fazer isso funcionar.

Definição de tipos M personalizados

A abordagem de imposição de esquema na lição anterior usou "tabelas de esquema" definidas como pares Nome/Tipo. Ele funciona bem ao trabalhar com dados nivelados/relacionais, mas não oferece suporte à configuração de tipos em registros/tabelas/listas aninhados, nem permite que você reutilize definições de tipo em 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.

Aqui está uma rápida atualização sobre os tipos na linguagem M da especificação da linguagem:

Um valor de tipo é um valor que classifica outros valores. Diz-se que um valor classificado por um tipo está em conformidade com esse tipo. O sistema do tipo M consiste nos seguintes tipos de tipos:

  • Tipos primitivos, que classificam valores primitivos (binary, date, datetime, datetimezone, duration, list, numberrecordnulltexttimelogicaltype) e também incluem vários tipos abstratos (function, table, any, e )none
  • Tipos de registro, que classificam valores de registro com base em nomes de campo e tipos de valor
  • Tipos de lista, que classificam listas usando um único 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 classifica o valor null 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ê obtém (e/ou pesquisando as definições no $metadata do serviço), você pode definir os seguintes tipos de registro para representar tipos complexos 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
];

Observe como as LocationType referências a CityType e LocType para representar suas colunas estruturadas.

Para as entidades de nível superior (que você deseja representar como Tabelas), você define 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, atualize sua SchemaTable variável (que você usa como uma "tabela de pesquisa" para mapeamentos de entidade para digitar) para usar estas novas definições de tipo:

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

Impondo um esquema usando tipos

Você confiará em uma função comum (Table.ChangeType) para impor um esquema em seus dados, muito parecido com o que usou SchemaTransformTable na lição anterior. Ao contrário SchemaTransformTabledo , Table.ChangeType usa um tipo de tabela M real como um argumento e aplicará seu esquema recursivamente para todos os tipos aninhados. A sua assinatura tem o seguinte aspeto:

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

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

Nota

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

Em seguida, você precisa atualizar o código do conector para alterar o schema parâmetro de a table para um typee adicionar uma chamada para Table.ChangeType in 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];

Confirmando que os tipos aninhados estão sendo definidos

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

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

Teste unitário com sucesso.

Refatoração de código comum em arquivos separados

Nota

O motor M terá um suporte melhorado para referenciar módulos externos/código comum no futuro, mas esta abordagem deve levá-lo até lá.

Neste ponto, sua extensão quase tem 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 integrada ou você poderá fazer referência a elas a partir 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 Build Action no arquivo como Compile para garantir que ela seja incluída no arquivo de extensão durante a compilação.
  3. Defina uma função para carregar o código usando Expression.Evaluate.
  4. Carregue cada uma das funções comuns que deseja usar.

O código para fazer isso está incluído no trecho 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 que você obtém de uma API REST. O conector está atualmente codificando suas informações de esquema, o que tem um benefício de desempenho em tempo de execução, mas é incapaz de se adaptar às alterações nos metadados do serviço ao longo do tempo. Os tutoriais futuros passarão para uma abordagem puramente dinâmica que inferirá o esquema a partir 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 para melhorar a legibilidade geral.

Próximos passos

TripPin Parte 8 - Adicionando diagnósticos