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 e Date of Birth agrupados sob a nova categoria Employee.
  • 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 categoria Employment.
  • Valores de campos alterados ou mesclados: o campo Position na saída combina os campos Position e Office 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 para Employee.DateOfBirth sem conversão.
  • Mapeamento muitos para um: combina Position e Office em um único campo Employment.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 chamado position.

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 o captured 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 entre path1 e path3.
    • No final: path1.path2.* – O asterisco no final corresponde a qualquer segmento após path1.path2.
  • 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 como Saturation.
  • 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 os Captured Segments forem Properties.Opacity, o valor do Rank será 2. Se for apenas Opacity, o Rank será 1. Um mapeamento sem caracteres curinga tem um Rank 0.
    • Se o Rank dessa última regra for igual ou superior ao da regra anterior, um fluxo de dados irá tratá-la como uma Second Rule.
    • Caso contrário, o fluxo de dados tratará a configuração como um Specialization.

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.