Como usar expressões de agregação personalizadas no MongoDB 4.4
Avalie esse Tutorial
A próxima versão do MongoDB 4.4 torna mais fácil do que nunca trabalhar, transformar, acessar e dar sentido aos seus dados. Esta versão, o beta da qual você pode experimentar agora mesmo, vem com alguns novos operadores que tornam possível escrever funções personalizadas para estender a linguagem de query do MongoDB. Esse recurso, chamado Expressões de agregação personalizadas, permite escrever funções JavaScript que são executadas como parte de um estágio do pipeline de agregação. Eles são úteis quando você precisa implementar um comportamento que não é suportado pela linguagem de query do MongoDB por padrão.
A linguagem de query do MongoDB tem muitos operadores, ou funções, que permitem manipular e transformar seus dados para se adequar ao caso de uso do seu aplicativo. Operadores como $avg , $concate $filter facilitam para os desenvolvedores consultar, manipular e transformar seu conjunto de dados diretamente no nível do banco de dados, versus ter que escrever código adicional e transformar os dados em outro lugar. Embora haja operadores para quase tudo que você possa pensar, há alguns casos extremos em que um operador fornecido ou uma série de operadores não serão suficientes, e é aqui que entram as expressões de agregação customizadas.
Nesta publicação no blog, aprenderemos como podemos estender a linguagem de query do MongoDB para atender às nossas necessidades, escrevendo nossas próprias expressões de agregação personalizadas usando os novos operadores$function e $accumulator . Vamos mergulhar de cabeça!
Para este tutorial, você precisará de:
MongoDB 4.4 vem com dois novos operadores: $function e $accumulator . Esses dois operadores nos permitem escrever funções JavaScript personalizadas que podem ser usadas em um aggregation pipeline do MongoDB. Veremos exemplos de como usar ambos implementando nossas próprias expressões de agregação personalizadas.
Para obter o máximo valor desta postagem do blog, presumirei que você já esteja familiarizado com a estrutura de agregaçãodo MongoDB. Caso contrário, sugere-se verificar os Docs e seguir um tutorial ou dois para se familiarizar com como esse recurso funciona antes de abordar este tópico mais avançado.
Antes de entrarmos no código, quero falar brevemente sobre por que você se importaria com esse recurso em primeiro lugar. O primeiro motivo é oferecer maior desempenho aos seus usuários. Se você puder obter os dados exatos de que precisa diretamente do banco de dados em uma viagem, sem precisar fazer processamento e manipulação adicionais, poderá atender e atender às solicitações mais rapidamente. Em segundo lugar, as expressões de agregação personalizadas permitem que você cuide de casos extremos diretamente no estágio do pipeline de agregação. Se você já trabalhou com o pipeline de agregação no passado, se sentirá em casa e será produtivo rapidamente. Se você não estiver familiarizado com o pipeline de agregação, precisará aprendê-lo apenas uma vez. No momento em que você se encontrar com um caso de uso para os operadores
$function
ou $accumulator
, todo o seu conhecimento anterior será transferido. Acho que esses são dois motivos sólidos para se preocupar com expressões de agregação personalizadas: melhor desempenho para seus usuários e maior produtividade do desenvolvedor.A única ressalva ao uso liberal dos operadores
$function
e $accumulator
é o desempenho. A execução do JavaScript dentro de uma expressão de agregação consome muitos recursos e pode reduzir o desempenho. Você deve sempre optar por usar primeiro os operadores existentes e altamente otimizados, especialmente se eles puderem realizar o trabalho para o seu caso de uso. Somente considere o uso de $function
e $accumulator
se um operador existente não puder atender às necessidades de seu aplicativo.O primeiro operador que vamos dar uma olhada é chamado
$function
. Como o nome indica, esse operador permite que você implemente uma função JavaScript personalizada para implementar qualquer tipo de comportamento. A sintaxe deste operador é:1 { 2 $function: { 3 body: <code>, 4 args: <array expression>, 5 lang: "js" 6 } 7 }
O operador
$function
tem três propriedades. Obody
, que será nossa função JavaScript, uma arrayargs
contendo os argumentos que queremos passar para nossa função e uma propriedadelang
especificando a linguagem do nosso $function
, que a partir do MongoDB 4.4 é compatível apenas com JavaScript.A propriedade
body
mantém nossa função JavaScript como um tipo de código BSON ou string. Em nossos exemplos nesta postagem do blog, escreveremos nosso código como uma string. Nossa função JavaScript terá uma assinatura parecida com esta:1 function(arg){ 2 return arg 3 }
À primeira vista, parece uma função JavaScript padrão. Você pode passar em
n
argumentos e a função retorna um resultado. Os argumentos dentro da propriedadebody
serão mapeados para os argumentos fornecidos na propriedade de matrizargs
, então você precisará certifique-se de passar e capturar todos os argumentos fornecidos.Agora que sabemos as propriedades do operador
$function
, vamos usá-lo em um aggregation pipeline. Para começar, vamos escolher um conjunto de dados para trabalhar. Usaremos um dos conjuntos de dados de amostrado MongoDB fornecidos que você pode encontrar no MongoDB Atlas. Se você ainda não tiver um cluster configurado, pode fazê-lo criando uma conta gratuita do MongoDB Atlas. Carregar os conjuntos de dados de amostra é tão simples quanto clicar no botão "..." em seu cluster e selecionar a opção "Carregar conjunto de dados de amostra".Depois de carregar o conjunto de dados de amostra, vamos Go conectar ao nosso cluster MongoDB. Sempre que aprendo algo novo, prefiro usar uma abordagem visual, então, para este tutorial, vou contar com o MongoDB Compass. Se você já tiver o MongoDB Compass instalado, conecte-se ao cluster que tem o conjunto de dados de amostra carregado. Caso contrário, baixe a versão mais recente aqui e conecte-se.
Se você estiver usando o MongoDB Compass ou se conectando por meio do mongo shell, poderá encontrar sua connection string do MongoDB Atlas clicando no botão "Conectar" em seu cluster, escolhendo o tipo de aplicativo que usará para se conectar e copiando a string que será assim:
mongodb+srv://mongodb:<password>@cluster0-tdm0q.mongodb.net/test
.Quando você estiver conectado, o conjunto de dados com o qual trabalharemos se chamará
sample_mflix
e a coleção movies
. Go em frente, conecte-se a essa coleção e navegue até a guia " Agregações ". Para garantir que tudo funcione bem, vamos escrever um pipeline de agregação muito simples usando o novo $function
operador . No menu suspenso, selecione o $addFields
operador e adicione o seguinte código como sua implementação :1 { 2 fromFunction: {$function: {body: "function(){return 'hello'}", args: [], lang: 'js'}} 3 }
Se você estiver usando o mongo shell para executar essas queries, o código ficará assim:
1 db.movies.aggregate([ 2 { 3 $addFields: { 4 fromFunction: { 5 $function: { 6 body: "function(){return 'hello'}", 7 args: [], 8 lang: 'js' 9 } 10 } 11 } 12 } 13 ])
Se você examinar a saída no MongoDB Compass e rolar até a parte inferior de cada documento retornado, verá que cada documento agora tem um campo chamado
fromFunction
com o texto hello
como seu valor. Poderíamos ter simplesmente passado a string "olá" em vez de usar o operador$function
, mas o motivo pelo qual eu queria fazer isso foi garantir que sua versão do MongoDB Compass suportasse o operador$function
e essa é uma maneira mínima de testar ele.Em seguida, vamos implementar uma função personalizada que realmente faz algum trabalho. Vamos adicionar um novo campo a cada filme que tenha a pontuação de crítica de Ado, ou talvez a sua?
Vou nomear meu campo como
adoScore
. Agora, meu sistema de classificação é único. Dependendo do dia e do meu humor, posso gostar mais ou menos de um filme, portanto, começaremos a calcular a pontuação de Ado em um filme atribuindo-lhe aleatoriamente um valor entre 0 e 5. Assim, teremos uma base parecida com esta: let base = Math.floor(Math.random() * 6);
.Em seguida, se os críticos gostariam do filme, eu também, então vamos dizer que, se um filme tiver uma pontuação no IMDB de mais de 8, daremos +1 à pontuação de Ado. Caso contrário, deixaremos como está. Para isso, passaremos o campo
imdb.rating
para nossa função.Finalmente, os filmes que ganharam prêmios também recebem um impulso no sistema de pontuação do Ado. Então, para cada nomeação de prêmios que um filme recebe, a pontuação total do Ado aumentará em 0.25, e para cada prêmios ganhos, a pontuação aumentará em 0.5. Para calcular isso, teremos que fornecer o campo
awards
em nossa função também.Como nada é perfeito, adicionaremos uma regra personalizada à nossa função: se a pontuação total exceder 10, apenas produziremos a pontuação final a ser 9.9. Vamos ver como é toda essa função:
1 { 2 adoScore: {$function: { 3 body: "function(imdb, awards){let base = Math.floor(Math.random() * 6) \n let imdbBonus = 0 \n if(imdb > 8){ imdbBonus = 1} \n let nominations = (awards.nominations * 0.25) \n let wins = (awards.wins * 0.5) \n let final = base + imdbBonus + nominations + wins \n if(final > 10){final = 9.9} \n return final}", 4 args: ["$imdb.rating", "$awards"], 5 lang: 'js'}} 6 }
Para facilitar a leitura da função JavaScript, aqui está ela em formato não string:
1 function(imdb, awards){ 2 let base = Math.floor(Math.random() * 6) 3 let imdbBonus = 0 4 5 if(imdb > 8){ imdbBonus = 1} 6 7 let nominations = awards.nominations * 0.25 8 let wins = awards.wins * 0.5 9 10 let final = base + imdbBonus + nominations + wins 11 if(final > 10){final = 9.9} 12 13 return final 14 }
E, novamente, se você estiver usando o mongo shell, o código será parecido:
1 db.movies.aggregate([ 2 { 3 $addFields: { 4 adoScore: { 5 $function: { 6 body: "function(imdb, awards){let base = Math.floor(Math.random() * 6) \n let imdbBonus = 0 \n if(imdb > 8){ imdbBonus = 1} \n let nominations = (awards.nominations * 0.25) \n let wins = (awards.wins * 0.5) \n let final = base + imdbBonus + nominations + wins \n if(final > 10){final = 9.9} \n return final}", 7 args: ["$imdb.rating", "$awards"], 8 lang: 'js' 9 } 10 } 11 } 12 } 13 ])
A execução da agregação
$addFields
acima , que usa o operador$function
, produzirá um resultado que adiciona um novo campoadoScore
ao final de cada documento. Este campo conterá um valor numérico que varia de 0 a 9.9. No operador$function
, passamos nossa função JavaScript personalizada para a propriedadebody
. À medida que iterávamos em nossos documentos, os campos$imdb.rating
e $awards
de cada documento foram passados para nossa função personalizada.Usando a notação de ponto, vimos como especificar qualquer subdocumento que você queira usar em uma agregação. Também aprendemos a usar um campo inteiro e seus subcampos em uma agregação, como vimos com o parâmetro
$awards
em nosso exemplo anterior. Nosso resultado final tem a seguinte aparência:Isso é apenas uma amostra do que podemos fazer com o operador
$function
. Em nosso exemplo acima, emparelhá-lo com o operador$addFields
, mas também podemos usar $function
como alternativa ao operador$where
ou também com outros operadores. Confira o $function
docs para obter mais informações.O próximo operador que examinaremos, que também nos permite escrever funções JavaScript personalizadas, é chamado de operador$accumulator e é um pouco mais complexo. Este operador nos permite definir uma função de acumulador personalizada com JavaScript. Os acumuladores são operadores que mantêm seu estado à medida que os documentos progridem no pipeline. Grande parte das mesmas regras se aplicam ao operador
$accumulator
e$function
. Começaremos dando uma olhada na sintaxe do operador$accumulator
:1 { 2 $accumulator: { 3 init: <code>, 4 initArgs: <array expression>, // Optional 5 accumulate: <code>, 6 accumulateArgs: <array expression>, 7 merge: <code>, 8 finalize: <code>, // Optional 9 lang: <string> 10 } 11 }
Temos alguns campos adicionais para discutir. Em vez de apenas um campo
body
que contém uma função JavaScript, o operador$accumulator
nos dá quatro locais adicionais para escrever JavaScript:- O campo
init
que inicializa o estado do acumulador. - O campo
accumulate
que acumula os documentos que passam pelo pipeline. - O campo
merge
usado para mesclar vários estados. - O
finalize
campo que é usado para atualizar o resultado do acúmulo.
Para argumentos, temos dois locais para fornecê-los: o
initArgs
que é passado para nossa funçãoinit
e o accumulateArgs
que é passado para nossa funçãoaccumulate
. O processo para definir e passar os argumentos é o mesmo aqui e para o operador $function
. É importante observar que, para a funçãoaccumulate
, o primeiro argumento é state
e não o primeiro item na arrayaccumulateArgs
.Finalmente, temos que especificar o campo
lang
. Como antes, será js
, pois esse é o único idioma suportado a partir do MongoDB 4.4 liberação.Para ver um exemplo concreto do operador
$accumulator
em ação, continuaremos a usar nosso conjunto de dadossample_mflix
. Também criaremos em cima dos adoScore
que adicionamos com o operador$function
. Emparelharemos nosso $accumulator
com um operador de$group
e retornaremos o número de filmes lançados a cada ano de nosso conjunto de dados, bem como quantos filmes são considerados assistíveis pelo sistema de pontuação do Ado (o que significa que eles têm uma pontuação maior que 8). Nossa função$accumulator
ficará assim:1 { 2 _id: "$year", 3 consensus: { 4 $accumulator: { 5 init: "function(){return {total:0, worthWatching: 0}}", 6 accumulate: "function(state, adoScore){let worthIt = 0; if(adoScore > 8){worthIt = 1}; return {total:state.total + 1, worthWatching: state.worthWatching + worthIt }}", 7 accumulateArgs:["$adoScore"], 8 merge: "function(state1, state2){return {total: state1.total + state2.total, worthWatching: state1.worthWatching + state2.worthWatching}}", 9 } 10 } 11 }
E apenas para exibir as funções JavaScript em formato não string para legibilidade:
1 // Init 2 function(){ 3 return { total:0, worthWatching: 0 } 4 } 5 6 // Accumulate 7 function(state, adoScore){ 8 let worthIt = 0; 9 if(adoScore > 8){ worthIt = 1}; 10 return { 11 total: state.total + 1, 12 worthWatching: state.worthWatching + worthIt } 13 } 14 15 // Merge 16 function(state1, state2){ 17 return { 18 total: state1.total + state2.total, 19 worthWatching: state1.worthWatching + state2.worthWatching 20 } 21 }
Se você estiver executando a agregação acima usando o mongo shell, a consulta será parecida com a seguinte:
1 db.movies.aggregate([ 2 { 3 $group: { 4 _id: "$year", 5 consensus: { 6 $accumulator: { 7 init: "function(){return {total:0, worthWatching: 0}}", 8 accumulate: "function(state, adoScore){let worthIt = 0; if(adoScore > 8){worthIt = 1}; return {total:state.total + 1, worthWatching: state.worthWatching + worthIt }}", 9 accumulateArgs:["$adoScore"], 10 merge: "function(state1, state2){return {total: state1.total + state2.total, worthWatching: state1.worthWatching + state2.worthWatching}}", 11 } 12 } 13 } 14 } 15 ])
O resultado da execução desta query no
sample_mflix
banco de dados será parecido com isso:Observação: como a função
adoScore
depende de Math.random()
para parte de seus cálculos, você pode obter resultados variáveis sempre que executar a agregação.Assim como o operador
$function
, escrever um acumulador personalizado e usar o operador$accumulator
só deve ser feito quando os operadores existentes não puderem atender ao caso de uso do seu aplicativo. Da mesma forma, também estamos apenas arranhando a superfície do que é possível fazer ao escrever seu próprio acumulador. Consulte a Docs para saber mais.Antes de encerrarmos esta postagem do blog, vamos dar uma olhada em como será o nosso pipeline de agregação completo, combinando os operadores
$function
e $accumulator
. Se estiver usando o conjunto de dadossample_mflix
, você poderá executar os dois exemplos com o seguinte código de pipeline de agregação:1 db.movies.aggregate([ 2 { 3 '$addFields': { 4 'adoScore': { 5 '$function': { 6 'body': 'function(imdb, awards){let base = Math.floor(Math.random() * 6) \n let imdbBonus = 0 \n if(imdb > 8){ imdbBonus = 1} \n let nominations = (awards.nominations * 0.25) \n let wins = (awards.wins * 0.5) \n let final = base + imdbBonus + nominations + wins \n if(final > 10){final = 9.9} \n return final}', 7 'args': [ 8 '$imdb.rating', '$awards' 9 ], 10 'lang': 'js' 11 } 12 } 13 } 14 }, { 15 '$group': { 16 '_id': '$year', 17 'consensus': { 18 '$accumulator': { 19 'init': 'function(){return {total:0, worthWatching: 0}}', 20 'accumulate': 'function(state, adoScore){let worthIt = 0; if(adoScore > 8){worthIt = 1}; return {total:state.total + 1, worthWatching: state.worthWatching + worthIt }}', 21 'accumulateArgs': [ 22 '$adoScore' 23 ], 24 'merge': 'function(state1, state2){return {total: state1.total + state2.total, worthWatching: state1.worthWatching + state2.worthWatching}}' 25 } 26 } 27 } 28 } 29 ])
Os novos operadores de
$function
e $accumulator
lançados no MongoDB 4.4 melhorar a produtividade do desenvolvedor e permitir que o MongoDB lide com muito mais casos extremos prontos para uso. Lembre-se de que esses novos operadores, embora poderosos, só devem ser usados se os operadores existentes não puderem fazer o trabalho, pois podem prejudicar o desempenho!Se você estiver tentando usar a nova funcionalidade com esses operadores, ajustando seu cluster MongoDB para obter um melhor desempenho, ou apenas tentando fazer mais com menos, o MongoDB 4.4 seguramente fornecerá algumas coisas novas e úteis para você. Você pode experimentar todas essas funcionalidades hoje mesmo implantando um MongoDB 4.4 cluster beta no MongoDB Atlas gratuitamente.
Se você tiver alguma dúvida sobre esses novos operadores ou sobre esta publicação,acesseos fóruns da MongoDB Community e nos encontraremos lá.
Boas experiências!
Declaração de Porto Seguro
O desenvolvimento, lançamento e cronograma de quaisquer recursos ou funcionalidades descritos para produtos MongoDB permanecem a critério exclusivo da MongoDB. Esta informação destina-se apenas a delinear a direção geral do nosso produto e não deve ser invocada na tomada de uma decisão de compra nem é um compromisso, promessa ou obrigação legal de entregar qualquer material, código ou funcionalidade. Exceto conforme exigido por lei, não assumimos nenhuma obrigação de atualizar quaisquer declarações prospectivas para refletir eventos ou circunstâncias após a data de tais declarações.