Mapear dados usando fluxos de dados
Importante
Esta página inclui instruções para gerenciar componentes do serviço Operações do Azure IoT usando manifestos de implantação do Kubernetes, que estão em versão prévia. Esse recurso é fornecido com várias limitações, e não deve ser usado para cargas de trabalho de produção.
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 transformação de dados nas Operações do Azure IoT. 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 sobre a linguagem de mapeamento de fluxo de dados e principais conceitos.
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 de metadados MQTT e Kafka
Ao usar MQTT ou Kafka como fonte ou destino, você pode acessar várias propriedades de metadados no idioma de mapeamento. Essas propriedades podem ser mapeadas na entrada ou saída.
Propriedades de metadados
-
Tópico: Funciona para ambos MQTT e Kafka. Contém a cadeia de caracteres em que a mensagem foi publicada. Exemplo:
$metadata.topic
. -
Propriedade do usuário: No MQTT, refere-se aos pares de chave/valor de forma livre que uma mensagem MQTT pode conter. Por exemplo, se a mensagem MQTT foi publicada com uma propriedade de usuário com chave "priority" e valor "high", então a referência
$metadata.user_property.priority
mantém o valor "high". As chaves de propriedade do usuário podem ser cadeias de caracteres arbitrárias e podem exigir escape:$metadata.user_property."weird key"
usa a chave "weird key" (com um espaço). -
Propriedade do sistema: Esse termo é usado para cada propriedade que não é uma propriedade do usuário. No momento, só há suporte para uma única propriedade do sistema:
$metadata.system_property.content_type
, que lê a propriedade do tipo de conteúdo da mensagem MQTT (se definida). -
Cabeçalho: Esse é o equivalente Kafka da propriedade do usuário MQTT. O Kafka pode usar qualquer valor binário para uma chave, mas os fluxos de dados dão suporte apenas a chaves de cadeia de caracteres UTF-8. Exemplo:
$metadata.header.priority
. Essa funcionalidade é semelhante às propriedades do usuário.
Mapeamento de propriedades de metadados
Mapeamento de entrada
No exemplo a seguir, a propriedade topic
do MQTT é mapeada para o campo origin_topic
na saída:
inputs: [
'$metadata.topic'
]
output: 'origin_topic'
Se a propriedade do usuário priority
estiver presente na mensagem MQTT, o exemplo a seguir demonstra como mapeá-la para um campo de saída:
inputs: [
'$metadata.user_property.priority'
]
output: 'priority'
Mapeamento de saída
Você também pode mapear propriedades de metadados para um cabeçalho de saída ou propriedade do usuário. 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'
Se a carga útil de entrada contiver um campo priority
, o exemplo a seguir demonstrará como mapeá-lo para uma propriedade de usuário MQTT:
inputs: [
'priority'
]
output: '$metadata.user_property.priority'
O mesmo exemplo para o Kafka:
inputs: [
'priority'
]
output: '$metadata.header.priority'
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 por notação de ponto pode incluir cadeias de caracteres e alguns caracteres especiais sem precisar escapar:
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 um fluxo de dados analise um caminho, ele 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: '*'
Esta configuração mostra um mapeamento básico em que cada campo na entrada é mapeado diretamente para o mesmo campo na saída sem alterações. O asterisco (*
) serve como um curinga que corresponde a qualquer campo no registro de entrada.
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.
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:
-
Único asterisco por referência de dados: Apenas um asterisco (
*
) é permitido dentro de uma única referência de dados. -
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 de uma referência de dados:
-
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: '$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
]
output: ''
}
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
da última regra for igual ou superior à regra anterior, um fluxo de dados a tratará como umaSecond Rule
. - Caso contrário, o fluxo de dados tratará a configuração como uma
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.