Mapear dados usando fluxos de dados
Importante
A Versão Prévia das Operações da Internet das Coisas do Azure – habilitadas pelo Azure Arc – está atualmente em versão prévia. Você não deve usar esse software em versão prévia em ambientes de produção.
Você precisará implantar uma nova instalação das Operações da Internet das Coisas do Azure quando uma versão em disponibilidade geral for disponibilizada. Você não poderá atualizar uma instalação de versão prévia.
Veja os Termos de Uso Complementares para Versões Prévias do Microsoft Azure para obter termos legais que se aplicam aos recursos do Azure que estão em versão beta, versão prévia ou que, de outra forma, ainda não foram lançados em disponibilidade geral.
Use a linguagem de mapeamento de fluxo de dados para transformar dados no Operações de IoT do Azure. A sintaxe é uma maneira simples, mas poderosa, de definir os mapeamentos que transformam um formato de dados em outro. Este artigo fornece uma visão geral da linguagem de mapeamento e dos principais conceitos de fluxo de dados.
O mapeamento permite transformar um formato de dados em outro. Pense no seguinte registro de entrada de dados:
{
"Name": "Grace Owens",
"Place of birth": "London, TX",
"Birth Date": "19840202",
"Start Date": "20180812",
"Position": "Analyst",
"Office": "Kent, WA"
}
Compare-o com o registro de saída:
{
"Employee": {
"Name": "Grace Owens",
"Date of Birth": "19840202"
},
"Employment": {
"Start Date": "20180812",
"Position": "Analyst, Kent, WA",
"Base Salary": 78000
}
}
No registro de saída, as seguintes alterações foram feitas nos dados do registro de entrada:
- Campos renomeados: o campo
Birth Date
agora éDate of Birth
. - Campos reestruturados: ambos
Name
eDate of Birth
agrupados sob a nova categoriaEmployee
. - Campo excluído: o campo
Place of birth
é removido porque não está presente na saída. - Campo adicionado: o campo
Base Salary
é um novo campo na categoriaEmployment
. - Valores de campos alterados ou mesclados: o campo
Position
na saída combina os camposPosition
eOffice
da entrada.
As transformações são obtidas por meio do mapeamento, que normalmente envolve:
- Definição de entrada: identifica os campos nos registros de entrada que foram utilizados.
- Definição de saída: especifica onde e como os campos de entrada são organizados nos registros de saída.
- Conversão (opcional): modifica os campos de entrada para que caibam nos campos de saída.
expression
é necessário quando diversos campos de entrada são combinados em um único campo de saída.
O mapeamento a seguir é um exemplo:
{
inputs: [
'BirthDate'
]
output: 'Employee.DateOfBirth'
}
{
inputs: [
'Position' // - - - - $1
'Office' // - - - - $2
]
output: 'Employment.Position'
expression: '$1 + ", " + $2'
}
{
inputs: [
'$context(position).BaseSalary'
]
output: 'Employment.BaseSalary'
}
O exemplo mapeia:
- Mapeamento um para um:
BirthDate
é mapeado diretamente paraEmployee.DateOfBirth
sem conversão. - Mapeamento muitos para um: combina
Position
eOffice
em um único campoEmployment.Position
. A fórmula de conversão ($1 + ", " + $2
) mescla esses campos em uma cadeia de caracteres formatada. - Dados contextuais:
BaseSalary
é adicionado a partir de um conjunto de dados contextuais chamadoposition
.
Referências de campo
As referências do campo mostram como especificar caminhos na entrada e saída usando notação de ponto como Employee.DateOfBirth
ou acessando dados de um conjunto de dados contextuais por meio de $context(position)
.
Propriedades do usuário do MQTT
Ao usar o MQTT como origem ou destino, você pode acessar as propriedades do usuário do MQTT no idioma de mapeamento. As propriedades do usuário podem ser mapeadas na entrada ou saída.
No exemplo a seguir, a propriedade topic
do MQTT é mapeada para o campo origin_topic
na saída.
inputs: [
'$metadata.topic'
]
output: 'origin_topic'
Você também pode mapear propriedades do MQTT para um cabeçalho de saída. No exemplo a seguir, o MQTT topic
é mapeado para o campo origin_topic
na propriedade de usuário da saída:
inputs: [
'$metadata.topic'
]
output: '$metadata.user_property.origin_topic'
Seletores de conjunto de dados de contextualização
Esses seletores permitem que os mapeamentos integrem dados extras de bancos de dados externos, que são chamados de conjuntos de dados de contextualização.
Filtragem de registros
A filtragem de registros envolve condições de configuração para selecionar quais registros devem ser processados e quais devem ser descartados.
Notação de ponto
A notação de ponto é amplamente usada na ciência da computação para referenciar campos, mesmo recursivamente. Em programação, os nomes de campos costumam consistir em letras e números. Um exemplo de notação de ponto padrão pode ser semelhante a este exemplo:
inputs: [
'Person.Address.Street.Number'
]
Em um fluxo de dados, um caminho descrito pela notação de ponto pode incluir cadeias de caracteres e alguns caracteres especiais sem precisar escapá-los:
inputs: [
'Person.Date of Birth'
]
Em outros casos, o escape é necessário:
inputs: [
'Person."Tag.10".Value'
]
O exemplo anterior, entre outros caracteres especiais, contém os ponto dentro do nome do campo. Sem escapar, o nome do campo serviria como um separador na notação de ponto em si.
Embora analise um caminho, um fluxo de dados trata apenas dois caracteres como especiais:
- Os ponto (
.
) atuam como separadores de campo. - Aspas simples, quando colocadas no início ou no final de um segmento, iniciam uma seção com escape em que os pontos não são tratados como separadores de campo.
Todos os outros caracteres são tratados como parte do nome do campo. Essa flexibilidade é útil em formatos como o JSON, em que os nomes de campo podem ser cadeias de caracteres arbitrárias.
No Bicep, todas as cadeias de caracteres são colocadas entre aspas simples ('
). Os exemplos sobre aspas adequadas em YAML para uso do Kubernetes não se aplicam.
Escape
A função principal de um escape em um caminho com notação de ponto é acomodar o uso de pontos que fazem parte de nomes de campo em vez de separadores:
inputs: [
'Payload."Tag.10".Value'
]
Neste exemplo, o caminho consiste em três segmentos: Payload
, Tag.10
e Value
.
Regras de escape na notação de ponto
Escape cada segmento separadamente: se vários segmentos contiverem pontos, esses segmentos deverão ser colocados entre aspas duplas. Outros segmentos também podem estar entre aspas, mas isso não afeta a interpretação do caminho:
inputs: [ 'Payload."Tag.10".Measurements."Vibration.$12".Value' ]
Uso adequado de aspas duplas: as aspas duplas devem abrir e fechar um segmento com escape. As aspas no meio do segmento são consideradas parte do nome do campo:
inputs: [ 'Payload.He said: "Hello", and waved' ]
Este exemplo define dois campos: Payload
e He said: "Hello", and waved
. Quando um ponto é exibido nestas circunstâncias, ele continua a servir como separador:
inputs: [
'Payload.He said: "No. It is done"'
]
Nesse caso, o caminho é dividido nos segmentos Payload
He said: "No
e It is done"
(começando com um espaço).
Algoritmos de segmentação
- Se o primeiro caractere de um segmento for uma aspa, o analisador procurará a próxima aspa. A cadeia de caracteres entre essas aspas é considerada um único segmento.
- Se o segmento não começar com uma aspa, o analisador identificará segmentos pesquisando o próximo ponto ou o final do caminho.
Curinga
Em muitos cenários, o registro de saída se assemelha muito ao registro de entrada, sendo que apenas modificações mínimas são necessárias. Quando você lida com registros que contêm vários campos, a especificação manual de mapeamentos para cada campo pode se tornar entediante. Os caracteres curinga simplificam esse processo ao permitir mapeamentos generalizados que podem ser aplicados automaticamente a vários campos.
Vamos levar em conta um cenário básico para entender o uso de asteriscos nos mapeamentos:
inputs: [
'*'
]
output: '*'
Veja como o asterisco (*
) opera nesse contexto:
- Padrões correspondentes: o asterisco pode corresponder a um único ou a vários segmentos de um caminho. Atua como um espaço reservado para todos os segmentos no caminho.
- Campos correspondentes: durante o processo de mapeamento, o algoritmo avalia cada campo no registro de entrada em relação ao padrão especificado no
inputs
. O asterisco no exemplo anterior corresponde a todos os caminhos possíveis, ajustando com eficácia cada campo individual na entrada. - Segmento capturado: a parte do caminho à qual o asterisco corresponde é conhecida como
captured segment
. - Mapeamento de saída: na configuração de saída, o
captured segment
está posicionado no local onde o asterisco aparece. Isso significa que a estrutura da entrada é preservada na saída, com ocaptured segment
preenchendo o espaço reservado fornecido pelo asterisco.
Essa configuração demonstra a forma mais genérica de mapeamento, em que cada campo na entrada é mapeado diretamente para um campo correspondente na saída, sem modificação.
Outro exemplo ilustra como os caracteres curinga podem ser usados para corresponder às subseções e movê-las juntas. Esse exemplo achata estruturas aninhadas dentro de um objeto JSON com eficácia.
JSON original:
{
"ColorProperties": {
"Hue": "blue",
"Saturation": "90%",
"Brightness": "50%",
"Opacity": "0.8"
},
"TextureProperties": {
"type": "fabric",
"SurfaceFeel": "soft",
"SurfaceAppearance": "matte",
"Pattern": "knitted"
}
}
Configuração de mapeamento que usa curingas:
{
inputs: [
'ColorProperties.*'
]
output: '*'
}
{
inputs: [
'TextureProperties.*'
]
output: '*'
}
JSON resultante:
{
"Hue": "blue",
"Saturation": "90%",
"Brightness": "50%",
"Opacity": "0.8",
"type": "fabric",
"SurfaceFeel": "soft",
"SurfaceAppearance": "matte",
"Pattern": "knitted"
}
Posicionamento do curinga
Ao colocar um curinga, você deve seguir estas regras:
- Asterisco único por dataDestination: somente um asterisco (
*
) é permitido em um único caminho. - Correspondência completa do segmento: o asterisco deve sempre corresponder a um segmento inteiro do caminho. Não pode ser usado para corresponder apenas a uma parte de um segmento, como
path1.partial*.path3
. - Posicionamento: o asterisco pode ser posicionado em várias partes do
dataDestination
:- No início:
*.path2.path3
– Aqui, o asterisco corresponde a qualquer segmento que leve atépath2.path3
. - No meio:
path1.*.path3
– Nessa configuração, o asterisco corresponde a qualquer segmento entrepath1
epath3
. - No final:
path1.path2.*
– O asterisco no final corresponde a qualquer segmento apóspath1.path2
.
- No início:
- O caminho que contém o asterisco deve ser colocado entre aspas simples (
'
).
Caracteres curinga com várias entradas
JSON original:
{
"Saturation": {
"Max": 0.42,
"Min": 0.67,
},
"Brightness": {
"Max": 0.78,
"Min": 0.93,
},
"Opacity": {
"Max": 0.88,
"Min": 0.91,
}
}
Configuração de mapeamento que usa curingas:
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*'
expression: '($1 + $2) / 2'
JSON resultante:
{
"ColorProperties" : {
"Saturation": 0.54,
"Brightness": 0.85,
"Opacity": 0.89
}
}
Se você usar curingas de várias entradas, o asterisco (*
) deverá representar consistentemente o mesmo Captured Segment
em todas as entradas. Por exemplo, quando *
captura a Saturation
no padrão *.Max
, o algoritmo de mapeamento espera que a Saturation.Min
correspondente corresponda ao padrão *.Min
. Aqui, *
é substituído pelo Captured Segment
da primeira entrada, orientando a equiparação para as entradas subsequentes.
Considere esse exemplo detalhado:
JSON original:
{
"Saturation": {
"Max": 0.42,
"Min": 0.67,
"Mid": {
"Avg": 0.51,
"Mean": 0.56
}
},
"Brightness": {
"Max": 0.78,
"Min": 0.93,
"Mid": {
"Avg": 0.81,
"Mean": 0.82
}
},
"Opacity": {
"Max": 0.88,
"Min": 0.91,
"Mid": {
"Avg": 0.89,
"Mean": 0.89
}
}
}
Configuração de mapeamento inicial que usa curingas:
inputs: [
'*.Max' // - $1
'*.Min' // - $2
'*.Avg' // - $3
'*.Mean' // - $4
]
Esse mapeamento inicial tenta criar uma matriz (por exemplo, para Opacity
: [0.88, 0.91, 0.89, 0.89]
). Essa configuração falha porque:
- A primeira entrada
*.Max
captura um segmento comoSaturation
. - O mapeamento espera que as entradas subsequentes estejam presentes no mesmo nível:
Saturation.Max
Saturation.Min
Saturation.Avg
Saturation.Mean
Como Avg
e Mean
estão aninhados dentro de Mid
, o asterisco no mapeamento inicial não captura corretamente esses caminhos.
Configuração de mapeamento corrigida:
inputs: [
'*.Max' // - $1
'*.Min' // - $2
'*.Mid.Avg' // - $3
'*.Mid.Mean' // - $4
]
Esse mapeamento revisado captura com precisão os campos necessários. Ele especifica corretamente os caminhos para incluir o objeto Mid
aninhado, o que garante que os asteriscos funcionem efetivamente em diferentes níveis da estrutura JSON.
Segunda regra versus especialização
Ao usar o exemplo anterior de curingas de várias entradas, considere os seguintes mapeamentos que geram dois valores derivados para cada propriedade:
{
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*.Avg'
expression: '($1 + $2) / 2'
}
{
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*.Diff'
expression: 'abs($1 - $2)'
}
Esse mapeamento se destina a criar dois cálculos separados (Avg
e Diff
) para cada propriedade no âmbito de ColorProperties
. Este exemplo mostra o resultado:
{
"ColorProperties": {
"Saturation": {
"Avg": 0.54,
"Diff": 0.25
},
"Brightness": {
"Avg": 0.85,
"Diff": 0.15
},
"Opacity": {
"Avg": 0.89,
"Diff": 0.03
}
}
}
Aqui, a segunda definição de mapeamento nas mesmas entradas atua como uma segunda regra para o mapeamento.
Agora, considere um cenário em que um campo específico precise de um cálculo diferente:
{
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*'
expression: '($1 + $2) / 2'
}
{
inputs: [
'Opacity.Max' // - $1
'Opacity.Min' // - $2
]
output: 'ColorProperties.OpacityAdjusted'
expression: '($1 + $2 + 1.32) / 2'
}
Nesse caso, o campo Opacity
tem um cálculo exclusivo. Duas opções para lidar com esse cenário sobreposto são as seguintes:
- Inclua ambos os mapeamentos para
Opacity
. Como os campos de saída são diferentes nesse exemplo, eles não se substituem. - Use a regra mais específica para
Opacity
e remova a mais genérica.
Considere um caso especial para os mesmos campos para ajudar a decidir a ação certa:
{
inputs: [
'*.Max' // - $1
'*.Min' // - $2
]
output: 'ColorProperties.*'
expression: '($1 + $2) / 2'
}
{
inputs: [
'Opacity.Max' // - $1
'Opacity.Min' // - $2
]
}
Um campo output
vazio na segunda definição implica em não gravar os campos no registro de saída (efetivamente removendo Opacity
). Essa configuração é mais uma Specialization
do que uma Second Rule
.
Resolução de mapeamentos sobrepostos por fluxos de dados:
- A avaliação progride a partir da regra principal na definição do mapeamento.
- Se um novo mapeamento for resolvido para os mesmos campos de uma regra anterior, as seguintes condições se aplicarão:
- Um
Rank
será calculado para cada entrada resolvida com base no número de segmentos que o caractere curinga capturar. Por exemplo, se osCaptured Segments
foremProperties.Opacity
, o valor doRank
será 2. Se for apenasOpacity
, oRank
será 1. Um mapeamento sem caracteres curinga tem umRank
0. - Se o
Rank
dessa última regra for igual ou superior ao da regra anterior, um fluxo de dados irá tratá-la como umaSecond Rule
. - Caso contrário, o fluxo de dados tratará a configuração como um
Specialization
.
- Um
Por exemplo, o mapeamento que direciona Opacity.Max
e Opacity.Min
para uma saída vazia tem um Rank
de 0. Como a segunda regra tem um Rank
menor que a anterior, ela é considerada uma especialização e substitui a regra anterior, que calcularia um valor para Opacity
.
Caracteres curinga em conjuntos de dados de contextualização
Agora, vamos ver como os conjuntos de dados de contextualização podem ser usados com curingas por meio de um exemplo. Considere um conjunto de dados chamado position
que contenha o seguinte registro:
{
"Position": "Analyst",
"BaseSalary": 70000,
"WorkingHours": "Regular"
}
Em um exemplo anterior, usamos um campo específico desse conjunto de dados:
inputs: [
'$context(position).BaseSalary'
]
output: 'Employment.BaseSalary'
Esse mapeamento copia BaseSalary
do conjunto de dados de contexto diretamente na seção Employment
do registro de saída. Se você quiser automatizar o processo e incluir todos os campos do conjunto de dados position
na seção Employment
, poderá usar curingas:
inputs: [
'$context(position).*'
]
output: 'Employment.*'
Essa configuração permite um mapeamento dinâmico em que cada campo dentro do conjunto de dados position
é copiado para a seção Employment
do registro de saída:
{
"Employment": {
"Position": "Analyst",
"BaseSalary": 70000,
"WorkingHours": "Regular"
}
}
Último valor conhecido
Você pode acompanhar o último valor conhecido de uma propriedade. Adicione ? $last
como sufixo do campo de entrada para capturar o último valor conhecido do campo. Quando falta um valor em uma propriedade em um conteúdo de entrada subsequente, o último valor conhecido é mapeado para o conteúdo de saída.
Por exemplo, considere as seguintes regras:
inputs: [
'Temperature ? $last'
]
output: 'Thermostat.Temperature'
Neste exemplo, o último valor Temperature
conhecido é acompanhado. Se um conteúdo de entrada subsequente não contiver um valor Temperature
, o último valor conhecido será usado na saída.