Construtores de expressões de agregação Java no MongoDB
Avalie esse Tutorial
Os pipelines de agregação do MongoDB permitem que os desenvolvedores criem processos avançados de recuperação, manipulação e atualização de documentos, expressos como uma sequência — ou pipeline — de estágios compostos, onde a saída de um estágio se torna a entrada para o próximo estágio do pipeline.
Com operações de agregação, é possível:
- Agrupar valores de diversos documentos.
- Remodele documentos.
- Executar operações de agregação nos dados agrupados para retornar um único resultado.
- Aplique operações especializadas a documentos, como funções geográficas, Full Text Search e funções de janela de tempo.
- Analisar alterações de dados ao longo do tempo.
A estrutura de aggregation aumentou desde sua introdução na versão 2 do MongoDB.2 a — a partir da versão 6.1 - abrange mais 35 estágios diferentes e mais 130 operadores diferentes.
Trabalhando com o shell do MongoDB ou em ferramentas como o MongoDB Compass, os pipelines de agregação são definidos como uma matriz de objetos BSON [1], com cada objeto definindo um estágio no pipeline. Em um sistema de loja online, um pipeline simples para encontrar todos os pedidos feitos entre 1 2023 e 31 2023 e, em seguida, fornecer uma contagem desses pedidos agrupados por tipo de produto, pode parecer como :
1 db.orders.aggregate( 2 [ 3 { 4 $match: 5 { 6 orderDate: { 7 $gte: ISODate("2023-01-01"), 8 }, 9 orderDate: { 10 $lte: ISODate("2023-03-31"), 11 }, 12 }, 13 }, 14 { 15 $group: 16 { 17 _id: "$productType", 18 count: { 19 $sum: 1 20 }, 21 }, 22 }, 23 ])
As expressões dão aos estágios do pipeline de agregação sua capacidade de manipular dados. Eles vêm em quatro formas:
Operadores: expressos como objetos com um prefixo de cifrão seguido pelo nome do operador. No exemplo acima, {$sum : 1} é um exemplo de um operador que incrementa a contagem de pedidos para cada tipo de produto em 1 sempre que um novo pedido para um tipo de produto é encontrado.
Caminhos de campo: expressos como strings com um prefixo de cifrão, seguido pelo caminho do campo. No caso de objetos ou matrizes incorporados, a notação de ponto pode ser usada para fornecer o caminho para o item incorporado. No exemplo acima, "$productType" é um caminho de campo.
Variáveis: expressas com um prefixo duplo de sinal de dólar, as variáveis podem ser definidas pelo sistema ou pelo usuário. Por exemplo, "$$NOW" retorna o valor de data e hora atual.
Valores literais: No exemplo acima, o valor literal ' 1' em {$sum : 1} pode ser considerado uma expressão e pode ser substituído por — por exemplo — uma expressão de caminho de campo.
Em aplicativos Java que usam os drivers nativos do MongoDB, aggregation pipelines podem ser definidos e executados construindo diretamente objetos de documento BSON equivalentes. Nosso exemplo de pipeline acima pode ter a seguinte aparência ao ser criado em Java usando essa abordagem:
1 … 2 MongoDatabase database = mongoClient.getDatabase("Mighty_Products"); 3 MongoCollection<Document> collection = database.getCollection("orders"); 4 5 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); 6 7 Bson matchStage = new Document("$match", 8 new Document("orderDate", 9 new Document("$gte", 10 formatter.parse("2023-01-01"))) 11 .append("orderDate", 12 new Document("$lte", 13 formatter.parse("2023-03-31")))); 14 15 Bson groupStage = new Document("$group", 16 new Document("_id", "$productType") 17 .append("count", 18 new Document("$sum", 1L))); 19 20 collection.aggregate( 21 Arrays.asList( 22 matchStage, 23 groupStage 24 ) 25 ).forEach(doc -> System.out.println(doc.toJson()));
O código Java acima é perfeitamente funcional e será executado conforme o esperado, mas destaca alguns problemas:
- Ao criar o código, tivemos que entender o formato dos documentos BSON correspondentes. Não foi possível utilizar os recursos do IDE, como autocompletar código e descoberta.
- Quaisquer erros na formatação dos documentos que estão sendo criados, ou nos parâmetros e tipos de dados passados para seus vários operadores, não serão identificados até que tentemos executar o código.
- Embora nosso exemplo acima seja relativamente simples, em pipelines mais complexos, o nível de indentação e aninhamento exigido no código de criação do documento correspondente pode levar a problemas de legibilidade.
Como alternativa à criação de objetos de documento BSON, o driver Java do MongoDB também define um conjunto de classes "builder " com métodos utilitários estáticos para simplificar a execução de muitas operações no MongoDB, incluindo a criação e a execução de aggregation pipeline stages. O uso das classes de construtores permite que os desenvolvedores descobrem mais erros na compilação em vez do tempo de execução e usa os recursos de descoberta e conclusão de código nos IDEs. Versões recentes do driver Java também adicionaram suporte estendido para operadores de expressão ao usar as classes de construtor de agregação, permitindo que pipelines sejam escritas com métodos seguros de tipo e usando padrões de codificação fluentes.
Usando essa abordagem, o código acima pode ser escrito como:
1 MongoDatabase database = mongoClient.getDatabase("Mighty_Products"); 2 MongoCollection<Document> collection = database.getCollection("orders"); 3 4 var orderDate = current().getDate("orderDate"); 5 Bson matchStage = match(expr(orderDate.gte(of(Instant.parse("2023-01-01"))) 6 .and(orderDate.lte(of(Instant.parse("2023-03-31")))))); 7 8 Bson groupStage = group(current().getString("productType"), sum("count", 1L)); 9 10 collection.aggregate( 11 Arrays.asList( 12 matchStage, 13 groupStage 14 ) 15 ).forEach(doc -> System.out.println(doc.toJson()));
No restante deste artigo, examinaremos um exemplo de pipeline de agregação usando as classes e os métodos do construtor de agregação e destacaremos alguns dos novos suportes do operador de expressão de agregação.
Nosso exemplo de aggregation pipeline é baseado em um banco de dados que coleta e analisa dados do Controle de Tráfego Aéreo transmitidos por naves embarcando e partindo do aeroporto internacional de Denver. Os dados são coletados usando um receptor construído usando um Raspberry Pi e endereços definidos por software (SDRs) usando software do excepcional projeto de código abertoStratux.
Esses receptores baratos de construir tornaram-se populares entre os pilotos de aeronaves leves nos últimos anos, pois permitem que eles projetem a localização de aeronaves próximas na exibição de mapas de aplicativos de navegação baseados em tablets e smartphones, como o Foreflight, ajudando a evitar colisões no ar.

Em nossa aplicação, os dados recebidos do receptor Stratux são combinados com dados de referência de aeronaves da Rede Opensky para nos fornecer documentos parecidos com estes:
1 { 2 "_id": { 3 "$numberLong": "11262117" 4 }, 5 "model": "B737", 6 "tailNum": "N8620H", 7 "positionReports": [ 8 { 9 "callsign": "SWA962", 10 "alt": { 11 "$numberLong": "12625" 12 }, 13 "lat": { 14 "$numberDecimal": "39.782833" 15 }, 16 "lng": { 17 "$numberDecimal": "-104.49988" 18 }, 19 "speed": { 20 "$numberLong": "283" 21 }, 22 "track": { 23 "$numberLong": "345" 24 }, 25 "vvel": { 26 "$numberLong": "-1344" 27 }, 28 "timestamp": { 29 "$date": "2023-01-31T23:28:26.294Z" 30 } 31 }, 32 { 33 "callsign": "SWA962", 34 "alt": { 35 "$numberLong": "12600" 36 }, 37 "lat": { 38 "$numberDecimal": "39.784744" 39 }, 40 "lng": { 41 "$numberDecimal": "-104.50058" 42 }, 43 "speed": { 44 "$numberLong": "283" 45 }, 46 "track": { 47 "$numberLong": "345" 48 }, 49 "vvel": { 50 "$numberLong": "-1344" 51 }, 52 "timestamp": { 53 "$date": "2023-01-31T23:28:26.419Z" 54 } 55 }, 56 { 57 "callsign": "SWA962", 58 "alt": { 59 "$numberLong": "12600" 60 }, 61 "lat": { 62 "$numberDecimal": "39.78511" 63 }, 64 "lng": { 65 "$numberDecimal": "-104.50071" 66 }, 67 "speed": { 68 "$numberLong": "283" 69 }, 70 "track": { 71 "$numberLong": "345" 72 }, 73 "vvel": { 74 "$numberLong": "-1344" 75 }, 76 "timestamp": { 77 "$date": "2023-01-31T23:28:26.955Z" 78 } 79 } 80 ] 81 }
O campo "tailNum" fornece o número de registro exclusivo da aeronave e não muda entre os relatórios de posição. Os relatórios de posição estão em uma matriz [2], com cada entrada fornecendo as coordenadas geográficas da aeronave, sua altitude, velocidade (horizontal e vertical), rumo e data e hora. Os relatórios de posição também fornecem o indicativo do voo que a aeronave estava operando no momento em que transmitiu o relatório de posição. Isso pode variar se os relatórios de posição da aeronave foram coletados quando ela voou para Denver e, mais tarde, quando saiu de Denver operando um voo diferente. Na amostra acima, a nave N8620H, um boeing 737, estava operando o voo SWA962 — um voo da Sudoeste Airline. Ele estava flutuando a uma velocidade de 283 nós, em uma proa de 345 graus, descendentes por 12,600 metros a 1344 ft/minuto.
Usando dados coletados em um período de 36horas, nossa coleção contém informações sobre mais de 500 aeronaves diferentes e mais de meio milhão de relatórios de posição. Queremos construir um pipeline de agregação que mostre o número de diferentes aeronaves operadas pela United Airlines agrupadas por tipo de aeronave.
O aggregation pipeline que executaremos em nossos dados consistirá em três estágios:
O primeiro - um estágiode correspondência - encontrará todas as aeronaves que transmitiram um indicativo da United Airlines entre duas datas.
A seguir, realizaremos um estágio de grupo que pegará os documentos da aeronave encontrados no estágio de correspondência e criará um novo conjunto de documentos — um para cada modelo de aeronave encontrado durante o estágio de correspondência. Cada documento conterá uma lista de todos os números da cauda desse tipo de aeronave encontrados durante o estágio de correspondência.
Por fim, realizamos um estágiode projeto que é usado para remodelar os dados de cada documento em nosso formato final desejado.
Um estágio de correspondência realiza uma consulta para filtrar os documentos que estão sendo passados para a próxima etapa do pipeline. Um estágio de correspondência é normalmente usado como um dos primeiros estágios do pipeline para manter o número de documentos com os quais o pipeline precisa trabalhar — e, portanto, seu espaço de memória — em um tamanho razoável .
Em nosso pipeline, o estágio de correspondência selecionará todos os documentos da aeronave que contenham pelo menos um relatório de posição com um indicativo da United Airlines (todos os indicativos da United começam com o prefixo de três letras "UAL") e com um carimbo de data/hora entre cair dentro de um intervalo de datas selecionado. A representação BSON do estágio do pipeline resultante é semelhante a:
1 { 2 $match: { 3 positionReports: { 4 $elemMatch: { 5 callsign: /^UAL/, 6 $and: [ 7 { 8 timestamp: { 9 $gte: ISODate( 10 "2023-01-31T12:00:00.000-07:00" 11 ) 12 } 13 }, 14 { 15 timestamp: { 16 $lt: ISODate( 17 "2023-02-01T00:00:00.000-07:00" 18 ) 19 } 20 } 21 ] 22 } 23 } 24 } 25 }
O operador$elemMatch especifica que os critérios de consulta que fornecemos devem ocorrer todos em uma única entrada em uma matriz para gerar uma correspondência, de modo que um documento de aeronave só corresponderá se contiver pelo menos um relatório de posição em que o indicativo comece com "UAL" e o carimbo de data/hora esteja entre 12:00 em 31de janeiro e 00:00 em 1de fevereiro no fuso horário da montanha.
Em Java, depois de usar o Maven ou o Gradle para adicionar os drivers do MongoDB Java como uma dependência em nosso projeto, poderíamos definir esse estágio criando um objeto de documento BSON equivalente:
1 //Create the from and to dates for the match stage 2 String sFromDate = "2023-01-31T12:00:00.000-07:00"; 3 TemporalAccessor ta = DateTimeFormatter.ISO_INSTANT.parse(sFromDate); 4 Instant fromInstant = Instant.from(ta); 5 Date fromDate = Date.from(fromInstant); 6 7 String sToDate = "2023-02-01T00:00:00.000-07:00"; 8 ta = DateTimeFormatter.ISO_INSTANT.parse(sToDate); 9 Instant toInstant = Instant.from(ta); 10 Date toDate = Date.from(toInstant); 11 12 Document matchStage = new Document("$match", 13 new Document("positionReports", 14 new Document("$elemMatch", 15 new Document("callsign", Pattern.compile("^UAL")) 16 .append("$and", Arrays.asList( 17 new Document("timestamp", new Document("$gte", fromDate)), 18 new Document("timestamp", new Document("$lt", toDate)) 19 )) 20 ) 21 ) 22 );
Como vimos com o exemplo anterior da loja online, embora esse código seja perfeitamente funcional, precisamos entender a estrutura do documento BSON correspondente, e quaisquer erros que cometemos na construção só seriam descobertos em tempo de execução.
Como alternativa, depois de adicionar as instruções de importação necessárias para dar ao nosso código acesso aos métodos estáticos do construtor de agregação e do operador de expressão, podemos criar um estágio de pipeline equivalente com o seguinte código:
1 import static com.mongodb.client.model.Aggregates.*; 2 import static com.mongodb.client.model.Filters.*; 3 import static com.mongodb.client.model.Projections.*; 4 import static com.mongodb.client.model.Accumulators.*; 5 import static com.mongodb.client.model.mql.MqlValues.*; 6 //... 7 8 //Create the from and to dates for the match stage 9 String sFromDate = "2023-01-31T12:00:00.000-07:00"; 10 TemporalAccessor ta = DateTimeFormatter.ISO_INSTANT.parse(sFromDate); 11 Instant fromInstant = Instant.from(ta); 12 13 String sToDate = "2023-02-01T00:00:00.000-07:00"; 14 ta = DateTimeFormatter.ISO_INSTANT.parse(sToDate); 15 Instant toInstant = Instant.from(ta); 16 17 var positionReports = current().<MqlDocument>getArray("positionReports"); 18 Bson matchStage = match(expr( 19 positionReports.any(positionReport -> { 20 var callsign = positionReport.getString("callsign"); 21 var ts = positionReport.getDate("timestamp"); 22 return callsign 23 .substr(0,3) 24 .eq(of("UAL")) 25 .and(ts.gte(of(fromInstant))) 26 .and(ts.lt(of(toInstant))); 27 }) 28 ));
Há algumas coisas que vale a pena observar neste código:
Em primeiro lugar, a estrutura de operadores de expressões nos dá acesso a um método current() que retorna o documento que está sendo processado no momento pelo pipeline de agregação. Nós o usamos inicialmente para obter a matriz de relatórios de posição do documento atual.
Em seguida, embora estejamos usando o método construtor de agregaçãomatch() para criar nosso estágio de match, para demonstrar melhor o uso da estrutura de operadores de expressão e seu estilo de codificação associado, usamos o filtroexpr()[3] construtor para criar uma expressão que usa o operador de expressão de arrayany() para iterar cada entrada na arraypositionReports, procurando por qualquer um que corresponda ao nosso predicado — ou seja, que tenha um campo de indicativo que começa com as letras "UAL " e um carimbo de data/hora dentro do nosso intervalo de data/hora especificado. Isso é equivalente ao que o operador$elemMatch em nosso estágio original do pipeline baseado em documentos BSON estava fazendo.
Além disso, ao usar os operadores de expressão para recuperar campos, usamos métodos específicos de tipo para indicar o tipo do valor de retorno esperado. Ocallsign foi recuperado usando getString(), enquanto a variável timestamp ts foi recuperada usando getDate(). Isso permite que IDEs como IntelliJ e Visual Studio Code executem a verificação de tipo e que a conclusão subsequente do código seja personalizada para mostrar apenas métodos e documentação relevantes para o tipo retornado. Isso pode levar a uma codificação mais rápida e menos propensa a erros.

Por fim, observe que, ao criar o predicado para o operador de expressãoany(), usamos um estilo de codificação fluente e elementos de codificação idiossincráticos, como lambdas, com os quais muitos desenvolvedores de Java estarão familiarizados e se sentirão mais confortáveis em usar, em vez da abordagem específica do MongoDB necessária para criar diretamente documentos BSON.
Depois de filtrar nossa lista de documentos para incluir apenas aeronaves operadas pela United Airlines em nosso estágio de correspondência, no segundo estágio do pipeline, realizamos uma operaçãode grupo para iniciar a tarefa de contar o número de aeronaves de cada modelo. O documento BSON para esse estágio tem a seguinte aparência:
1 { 2 $group: 3 { 4 _id: "$model", 5 aircraftSet: { 6 $addToSet: "$tailNum", 7 }, 8 }, 9 }
Nesta etapa, estamos especificando que queremos agrupar os dados do documento pelo campo "model" e que em cada documento resultante, queremos uma matriz chamada "aircraftSet" contendo cada número de cauda exclusivo de aeronaves observadas desse tipo de modelo. A saída dos documentos desta etapa é semelhante a:
1 { 2 "_id": "B757", 3 "aircraftSet": [ 4 "N74856", 5 "N77865", 6 "N17104", 7 "N19117", 8 "N14120", 9 "N57855", 10 "N77871" 11 ] 12 }
O código Java correspondente para o estágio tem a seguinte aparência:
1 Bson bGroupStage = group(current().getString("model"), 2 addToSet("aircraftSet", current().getString("tailNum")));
Como antes, usamos o método expressions framework current() para acessar o documento que está sendo processado atualmente pelo pipeline. O método de acumuladoraddToSet()dos construtores de agregação é usado para garantir que apenas números finais exclusivos sejam adicionados ao array "aircraftSet".
- Renomeia o campo "_id " introduzido pela fase de grupos de volta para "model. "
- Troque a array de números de cauda pelo número de entradas na array.
- Adicione um novo campo, "airline, " preenchendo-o com o valor literal "United. "
- Adicione um campo chamado “manufacturer” e use um operador condicional $cond para preenchê-lo com:
- “AIRBUS” se o modelo de avião começar com “A.”
- “BOEING " se começar com um "B. "
- “CANADAIR " se começar com um "C. "
- "EMBRAER " se começar com um "E. "
- "MCDONNELL DOUGLAS " se começar com um "M. "
- “UNKNOWN” em todos os outros casos.
O documento BSON para esta etapa é semelhante a:
1 { 2 $project: { 3 airline: "United", 4 model: "$_id", 5 count: { 6 $size: "$aircraftSet", 7 }, 8 manufacturer: { 9 $let: { 10 vars: { 11 manufacturerPrefix: { 12 $substrBytes: ["$_id", 0, 1], 13 }, 14 }, 15 in: { 16 $switch: { 17 branches: [ 18 { 19 case: { 20 $eq: [ 21 "$$manufacturerPrefix", 22 "A", 23 ], 24 }, 25 then: "AIRBUS", 26 }, 27 { 28 case: { 29 $eq: [ 30 "$$manufacturerPrefix", 31 "B", 32 ], 33 }, 34 then: "BOEING", 35 }, 36 { 37 case: { 38 $eq: [ 39 "$$manufacturerPrefix", 40 "C", 41 ], 42 }, 43 then: "CANADAIR", 44 }, 45 { 46 case: { 47 $eq: [ 48 "$$manufacturerPrefix", 49 "E", 50 ], 51 }, 52 then: "EMBRAER", 53 }, 54 { 55 case: { 56 $eq: [ 57 "$$manufacturerPrefix", 58 "M", 59 ], 60 }, 61 then: "MCDONNELL DOUGLAS", 62 }, 63 ], 64 default: "UNKNOWN", 65 }, 66 }, 67 }, 68 }, 69 _id: "$$REMOVE", 70 }, 71 }
Os documentos de saída resultantes são apresentados assim:
1 { 2 "airline": "United", 3 "model": "B777", 4 "count": 5, 5 "Manufacturer": "BOEING" 6 }
O código Java para este estágio é semelhante a:
1 Bson bProjectStage = project(fields( 2 computed("airline", "United"), 3 computed("model", current().getString("_id")), 4 computed("count", current().<MqlDocument>getArray("aircraftSet").size()), 5 computed("manufacturer", current() 6 .getString("_id") 7 .substr(0, 1) 8 .switchStringOn(s -> s 9 .eq(of("A"), (m -> of("AIRBUS"))) 10 .eq(of("B"), (m -> of("BOEING"))) 11 .eq(of("C"), (m -> of("CANADAIR"))) 12 .eq(of("E"), (m -> of("EMBRAER"))) 13 .eq(of("M"), (m -> of("MCDONNELL DOUGLAS"))) 14 .defaults(m -> of("UNKNOWN")) 15 )), 16 excludeId() 17 ));
Observe novamente o uso de métodos de acesso de campo específicos do tipo para obter o tipo de modelo de aeronave (string) e aircraftSet (array do tipo MQLDocument). Ao determinar o fabricante da aeronave, usamos novamente um estilo de codificação fluente para definir condicionalmente o valor para Boeing ou Airbus.
Com os três estágios do pipeline agora definidos, agora podemos executar o pipeline em nossa collection:
1 aircraftCollection.aggregate( 2 Arrays.asList( 3 matchStage, 4 groupStage, 5 projectStage 6 ) 7 ).forEach(doc -> System.out.println(doc.toJson()));
Se tudo correr conforme o planejado, isso deverá produzir uma saída para o console semelhante a:
1 {"airline": "United", "model": "B757", "count": 7, "manufacturer": "BOEING"} 2 {"airline": "United", "model": "B777", "count": 5, "manufacturer": "BOEING"} 3 {"airline": "United", "model": "A320", "count": 21, "manufacturer": "AIRBUS"} 4 {"airline": "United", "model": "B737", "count": 45, "manufacturer": "BOEING"}
Neste artigo, mostramos exemplos de como operadores de expressão e métodos construtores de agregação nas versões mais recentes dos drivers Java do MongoDB podem ser usados para construir pipelines de agregação usando um estilo de programação Java fluente e idiossincrático que pode utilizar a funcionalidade de preenchimento automático em IDEs e tipo- recursos do compilador de segurança. Isso pode resultar em um código mais robusto e de estilo mais familiar para muitos desenvolvedores Java. O uso das classes builder também coloca menos dependência de desenvolvedores com um amplo conhecimento do formato de documento BSON para estágios de pipeline de agregação.
Mais informações sobre o uso do construtor de agregação e das classes de operadores de expressão podem ser encontradas na documentação oficial do Driver Java do MongoDB .
O exemplo de código Java, o pipeline de agregação BSON e uma exportação JSON dos dados usados neste artigo podem ser encontrados no Github.
Mais informações
[1] O MongoDB utiliza o JSON binário (BSON) para armazenar dados e definir operações. BSON é um superconjunto de JSON, armazenado em formato binário e permitindo tipos de dados além daqueles definidos no padrão JSON. Obtenha mais informações sobre BSON.
[2] Deve-se observar que armazenar os relatórios de posição em uma array para cada avião como esse funciona bem para os fins do nosso exemplo, mas provavelmente não é o melhor design para um sistema de nível de produção, pois - com o tempo - as arrays para algumas naves podem se tornar excessivamente grandes. Uma discussão muito boa sobre arrays massivas e outros padrões anti, e como lidar com eles, está disponível em Developer Center.
[3] O uso de expressões nos estágios Aggregation Pipeline Match às vezes pode causar alguma confusão. Para uma discussão sobre isso e agregações em geral, o excelente e-book de Paul Done, “Practical MongoDB Aggregations,” é altamente recomendado.