Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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 el futuro 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 realizarse secuencialmente: cada lección se basa en el conector creado en las lecciones anteriores, agregando incrementalmente nuevas funcionalidades al conector.
En esta lección:
- Aplicación de un esquema de tabla mediante tipos M
- Establecer 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 usa el sistema de tipos M.
Esta lección le guía por los pasos siguientes:
- Agregar pruebas unitarias.
- Definición de tipos M personalizados.
- Aplicar 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, debe agregar un conjunto de pruebas unitarias al conector para reducir la posibilidad de interrumpir accidentalmente algo. Las pruebas unitarias funcionan de esta manera:
- Copie el código común del ejemplo UnitTest en el
TripPin.query.pqarchivo. - Agregue una declaración de sección a la parte superior del
TripPin.query.pqarchivo. - Cree un registro compartido (denominado
TripPin.UnitTest). - Defina un
Factpara cada prueba. - Llame
Facts.Summarize()a para ejecutar todas las pruebas. - Haga referencia a la llamada anterior como valor compartido para asegurarse de que se evalúa cuando se ejecuta 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 evalúan todos los hechos y se proporciona un informe similar al siguiente:
Con algunos principios del desarrollo controlado por pruebas, ahora agrega una prueba que produce un error actualmente, pero pronto se puede volver a implementar y corregir (al final de este tutorial). En concreto, se agrega 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 fallida.
Ahora solo tiene que implementar la funcionalidad para que esto funcione.
Definición de tipos M personalizados
El enfoque de cumplimiento de esquemas de la lección anterior usó "tablas de esquema" definidas como pares nombre/tipo. Funciona bien al trabajar con datos acoplados o relacionales, pero no admite la configuración de tipos en registros anidados, tablas o listas, o bien permite reutilizar definiciones de tipos en tablas o entidades.
En el caso de TripPin, los datos de las entidades People and 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, debe definir cada una de estas entidades mediante declaraciones de tipo M personalizadas.
Este es un breve recordatorio sobre los tipos del lenguaje M según 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 anulables, que clasifican el valor null además de los valores del tipo base
- Tipos de tipo, que clasifican valores que son tipos
Con la salida JSON sin procesar que obtiene (o busca las definiciones en el $metadata del servicio), puede 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 hace LocationType referencia a CityType y LocType para representar sus columnas estructuradas.
Para las entidades de nivel superior (que desea representar como tablas), defina los 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
];
A continuación, actualice su variable SchemaTable (que usa como una "tabla de búsqueda" para las asignaciones de entidad a tipo) para usar estas nuevas definiciones de tipo.
SchemaTable = #table({"Entity", "Type"}, {
{"Airlines", AirlinesType },
{"Airports", AirportsType },
{"People", PeopleType}
});
Aplicación de un esquema mediante tipos
Confías en una función común (Table.ChangeType) para aplicar un esquema a los datos, al igual que usas SchemaTransformTable en la lección anterior. A diferencia de SchemaTransformTable, Table.ChangeType toma un tipo de tabla M real como argumento y aplica el esquema de forma recursiva para todos los tipos anidados. Su firma tiene este aspecto:
Table.ChangeType = (table, tableType as type) as nullable table => ...
La lista de código completa de la Table.ChangeType función se puede encontrar en el archivo Table.ChangeType.pqm .
Nota:
Para mayor flexibilidad, la función se puede usar en tablas, así como listas de registros (que es cómo se representarían las tablas en un documento JSON).
A continuación, debe actualizar el código del conector para cambiar el schema parámetro de table a type, y agregar 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 en 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 devolverse type text en lugar de type any.
La ejecución de las pruebas unitarias muestran nuevamente que ahora todas pasan.
Refactorización de código común en archivos independientes
Nota:
El motor M tendrá un mejor soporte para hacer referencia a módulos externos o código común en el futuro, pero este enfoque debería ser suficiente por ahora.
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 podrá hacer referencia a ellas desde otra extensión. Por ahora, refactoriza el código de la siguiente manera:
- Mueva las funciones reutilizables a archivos independientes (.pqm).
- Establezca la propiedad Acción de generación del archivo a Compilar para garantizar 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 quiera usar.
El código para realizar esta refactorización se incluye en el siguiente fragmento de código:
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 obtiene de una API REST. El conector está codificando rígidamente su información de esquema, lo que tiene una ventaja de rendimiento en tiempo de ejecución, pero no se puede adaptar con el tiempo a los cambios en los metadatos del servicio. Los tutoriales futuros se trasladarán a un enfoque puramente dinámico que deducirá el esquema del documento de $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.