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:
- Adicionando testes de unidade.
- Definição de tipos M personalizados.
- Impondo um esquema usando tipos.
- 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:
- Copie o código comum do exemplo UnitTest para o
TripPin.query.pq
arquivo. - Adicione uma declaração de secção à parte superior do ficheiro
TripPin.query.pq
. - Crie um registro compartilhado (chamado
TripPin.UnitTest
). - Defina um
Fact
para cada teste. - Chamada
Facts.Summarize()
para executar todos os testes. - 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:
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.
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
,number
record
null
text
time
logical
type
) 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 SchemaTransformTable
do , 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 type
e 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.
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:
- Mova as funções reutilizáveis para arquivos separados (.pqm).
- 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.
- Defina uma função para carregar o código usando Expression.Evaluate.
- 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.