TripPin, parte 7: esquema avanzado con tipos M
Nota:
Actualmente, este contenido hace referencia al contenido de una implementación heredada para las pruebas unitarias en Visual Studio. El contenido se actualizará en un futuro próximo para cubrir el nuevo marco de pruebas del SDK de Power Query.
En este tutorial de varias partes se describe la creación de una nueva extensión de origen de datos para Power Query. El tutorial está diseñado para seguirse secuencialmente: cada lección se basa en el conector creado en las lecciones anteriores, agregando incrementalmente nuevas funcionalidades al conector.
En esta lección, aprenderá lo siguiente:
- Aplicación de un esquema de tabla mediante tipos M
- Establecimiento de tipos para listas y registros anidados
- Refactorización de código para reutilizar y pruebas unitarias
En la lección anterior, definió los esquemas de tabla mediante un sistema simple de "Tabla de esquemas". Este enfoque de tabla de esquemas funciona para muchas API REST o conectores de datos, pero los servicios que devuelven conjuntos de datos completos o profundamente anidados pueden beneficiarse del enfoque de este tutorial, que aprovecha el sistema de tipos M.
En esta lección se le guiará a través de los siguientes pasos:
- Adición de pruebas unitarias.
- Definición de tipos M personalizados.
- Aplicación de un esquema mediante tipos.
- Refactorización de código común en archivos independientes.
Adición de pruebas unitarias
Antes de empezar a usar la lógica de esquema avanzada, deberá agregar un conjunto de pruebas unitarias al conector para reducir la posibilidad de malograr algo accidentalmente. Las pruebas unitarias funcionan de esta manera:
- Copie el código común del ejemplo UnitTest en el archivo
TripPin.query.pq
. - Agregue una declaración de sección a la parte superior del archivo
TripPin.query.pq
. - Cree un registro compartido (denominado
TripPin.UnitTest
). - Defina un
Fact
para cada prueba. - Llame a
Facts.Summarize()
para ejecutar todas las pruebas. - Haga referencia a la llamada anterior como valor compartido para asegurarse de que se evalúe cuando se ejecute el proyecto en 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];
Al seleccionar Ejecutar en el proyecto se evaluarán todos los datos y se le proporcionará una salida de informe similar a la siguiente:
Aplicando algunos principios del desarrollo controlado por pruebas, ahora agregará una prueba que actualmente produce un error, pero pronto se volverá a implementar y se corregirá (al final de este tutorial). En concreto, agregará una prueba que comprueba uno de los registros anidados (correos electrónicos) que se devuelven en la entidad People.
Fact("Emails is properly typed", type text, Type.ListItem(Value.Type(People{0}[Emails])))
Si vuelve a ejecutar el código, debería ver que tiene una prueba con errores.
Ahora solo tiene que implementar la funcionalidad para que esto funcione.
Definición de tipos M personalizados
En el enfoque de aplicación de esquemas de la lección anterior se usaron "tablas de esquema" definidas como pares nombre/tipo. Funciona bien al trabajar con datos aplanados/relacionales, pero no admite tipos de configuración en tablas, listas o registros anidados, o bien permite reutilizar definiciones de tipos entre tablas o entidades.
En el caso de TripPin, los datos de las entidades People y Airports contienen columnas estructuradas e incluso comparten un tipo (Location
) para representar la información de dirección. En lugar de definir pares nombre/tipo en una tabla de esquema, deberá definir cada una de estas entidades mediante declaraciones de tipo M personalizadas.
A continuación se ofrece una actualización rápida de los tipos en el lenguaje M de la Especificación del lenguaje:
Un valor de tipo es un valor que clasifica otros valores. Se dice que un valor que está clasificado por un tipo se ajusta a ese tipo. El sistema de tipos de M consta de los tipos siguientes:
- Tipos primitivos, que clasifican valores primitivos (
binary
,date
,datetime
,datetimezone
,duration
,list
,logical
,null
,number
,record
,text
,time
,type
) e incluyen también una serie de tipos abstractos (function
,table
,any
ynone
)- Tipos de registro, que clasifican valores de registro en función de nombres de campo y tipos de valor
- Tipos de lista, que clasifican listas mediante un tipo base de un solo elemento
- Tipos de función, que clasifican valores de función según los tipos de sus parámetros y los valores devueltos
- Tipos de tabla, que clasifican valores de tabla en función de nombres de columna, tipos de columna y claves
- Tipos que aceptan valores NULL, que clasifican el valor NULL, además de todos los valores clasificados por un tipo base
- Tipos de tipo, que clasifican valores que son tipos
Con la salida JSON sin procesar que se obtiene (o mediante la búsqueda de las definiciones en los metadatos del servicio ), se pueden definir los siguientes tipos de registro para representar tipos complejos de 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 cómo LocationType
hace referencia a CityType
y LocType
para representar las columnas estructuradas.
Para las entidades de nivel superior (que se desee representar como Tablas), debe definir tipos de tabla:
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
];
Entonces se puede actualizar la variable SchemaTable
(que se puede usar como "tabla de búsqueda" para las asignaciones de entidad a tipo) para usar las nuevas definiciones de tipo:
SchemaTable = #table({"Entity", "Type"}, {
{"Airlines", AirlinesType },
{"Airports", AirportsType },
{"People", PeopleType}
});
Aplicación de un esquema mediante tipos
Confiará en una función común (Table.ChangeType
) para aplicar un esquema en los datos, de manera muy similar a como usó SchemaTransformTable
en la lección anterior.
A diferencia de SchemaTransformTable
, Table.ChangeType
toma un tipo de tabla M real como argumento y aplicará el esquema de forma recursiva para todos los tipos anidados. El aspecto de la firma es el siguiente:
Table.ChangeType = (table, tableType as type) as nullable table => ...
La lista de código completa de la función Table.ChangeType
se puede encontrar en el archivo Table.ChangeType.pqm.
Nota:
Para mayor flexibilidad, la función se puede usar en tablas, así como en listas de registros (como se representarían las tablas en un documento JSON).
A continuación, se deberá actualizar el código del conector para cambiar el parámetro schema
de table
a type
y añadir una llamada a Table.ChangeType
en 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
se actualiza para usar la lista de campos del esquema (para conocer los nombres de lo que se va a expandir al obtener los resultados), pero deja el cumplimiento real del esquema a 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];
Confirmación de que se están configurando tipos anidados
La definición de PeopleType
ahora establece el campo Emails
en una lista de texto ({text}
).
Si aplica correctamente los tipos, la llamada a Type.ListItem en la prueba unitaria ahora debería devolver type text
en lugar de type any
.
Al ejecutar de nuevo las pruebas unitarias, todas resultan satisfactorias.
Refactorización de código común en archivos independientes
Nota:
El motor M contará con una mejor compatibilidad para hacer referencia a módulos externos o código común en el futuro, pero este enfoque debería servirle hasta entonces.
En este momento, la extensión casi tiene tanto código "común" como código de conector TripPin. En el futuro, estas funciones comunes formarán parte de la biblioteca de funciones estándar integrada o se podrá hacer referencia a ellas desde otra extensión. Por ahora, debe refactorizar el código de la siguiente manera:
- Traslade las funciones reutilizables a archivos independientes (.pqm).
- Establezca la propiedad acción Acción de compilación en el archivo en Compilar para asegurarse de que se incluya en el archivo de extensión durante la compilación.
- Defina una función para cargar el código mediante Expression.Evaluate.
- Cargue cada una de las funciones comunes que desee usar.
El código para hacerlo se incluye en el fragmento de código siguiente:
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");
Conclusión
En este tutorial se han realizado varias mejoras en la forma de aplicar un esquema en los datos que se obtienen de una API REST. Actualmente el conector codifica de forma rígida su información de esquema, lo que ofrece una ventaja de rendimiento en tiempo de ejecución, pero no se puede adaptar a los cambios en el tiempo extra de los metadatos del servicio. En próximos tutoriales futuros se pasará a un enfoque puramente dinámico que inferirá el esquema del documento $metadata del servicio.
Además de los cambios de esquema, en este tutorial se agregaron pruebas unitarias para el código y se refactorizaron las funciones auxiliares comunes en archivos independientes para mejorar la legibilidad general.