Visão geral
Um pequeno helper de consulta para desenvolvedores de RDBMS que eram novos no MongoDB cresceu de acordo com as necessidades de projetos reais, explicando a história e os critérios de design até se tornar uma camada de conveniência MongoDB driver-first na versão 1.0.0.
História de origem
Desenvolvi principalmente em RDBMS por cerca de 4 anos e estava acostumado com ambientes operacionais, principalmente AWS RDS. Ao iniciar um novo projeto pessoal, escolhi o MongoDB pela primeira vez e, à medida que fui aprendendo, percebi que era uma ferramenta que poderia mudar significativamente minha produtividade de desenvolvimento, não apenas um banco de dados com esquema flexível.
Particularmente, o MongoDB Atlas parecia um novo mundo para mim. Eu poderia usar busca especializada e busca vetorial em uma única plataforma de dados, que antes achava que precisava de sistemas separados, e operações de manutenção como backup, monitoramento e escalabilidade eram muito mais convenientes do que eu estava acostumado com o AWS RDS. Se precisasse de uma escala e funcionalidades mais especializadas, poderia separar motores de busca ou bancos de dados vetoriais como serviços distintos, mas na fase de criar e operar rapidamente um projeto, a abrangência que o Atlas poderia resolver era muito atraente.
O problema era que, como era novo no MongoDB, escrever consultas dinâmicas era muito lento. No início, a forma findBy... do repositório era suficiente, mas à medida que as condições de busca se tornavam mais complexas, eu precisava montar manualmente Query, Criteria e expressões de operadores. A montagem repetitiva de condições em um estado não familiar era propensa a erros de digitação, e continuar escrevendo o mesmo tipo de código diminuía muito a velocidade de desenvolvimento.
Começou com um DSL simples para consulta
Portanto, o primeiro que criei foi uma pequena classe para simplificar a criação do R no CRUD. Este é o ponto de partida da classe que atualmente se chama ReactiveMongoDsl. Na época, não havia recursos como agregação, pipeline ou lookup, e era apenas uma forma de montar Query, Criteria e paginação um pouco mais rapidamente do que o ReactiveMongoTemplate do Spring Data. Eu achava que o restante poderia ser feito usando o ReactiveMongoTemplate básico ou o MongoDB Driver diretamente.
No entanto, a opinião do desenvolvedor com quem trabalhava era diferente. Ele também era novo no MongoDB e sempre perguntava se a DSL que eu criei tinha a funcionalidade necessária, como upsert ou bulk, sempre que precisava. Eu poderia ter dito para usar a API básica diretamente, mas pensei que ele também enfrentaria as mesmas dificuldades que eu tive ao aprender MongoDB pela primeira vez. Então, comecei a adicionar uma funcionalidade de cada vez à ReactiveMongoDsl sempre que surgia uma necessidade.
No início, tinha cerca de 600 linhas, e quando chegou a 1.000 linhas, ainda achava que não havia necessidade de dividir em várias classes. Quando chegou a cerca de 2.000 linhas, pensei um pouco, mas a estrutura para separação parecia estar crescendo ainda mais, e, acima de tudo, honestamente, era um trabalho chato. Quando ultrapassou 5.000 linhas, não pude mais adiar e movi algumas funcionalidades que poderiam ser separadas para fora, mas já estava em um estado em que era difícil dividir o fluxo central em partes menores. A razão pela qual a classe central é grande não é porque eu projetei uma DSL gigantesca desde o início, mas sim pela história de acumular continuamente funcionalidades necessárias em um único ponto de entrada em projetos reais.
Assim, após adicionar consulta, agregação, lookup, atualizações atômicas, bulk, histórico, Atlas Search e Vector Search, em algum momento, a maioria das funcionalidades que eu frequentemente precisava ao usar MongoDB em projetos Java já estava incluída. Em vez de mantê-las como helpers internos de um único projeto, separei-as para que pudessem ser usadas da mesma forma em vários projetos, e esse é o processo de nascimento do reactive-mongo-dsl.
De helper do Spring para biblioteca driver-first
O ponto de partida era um helper para usar ReactiveMongoTemplate de forma mais conveniente, mas à medida que as funcionalidades aumentaram e começaram a ser reutilizadas em vários projetos, percebi que o modelo de execução de um framework específico não deveria ser o limite da biblioteca. O núcleo da versão 1.0.0 usa MongoExecutionContext como contrato de execução e utiliza diretamente o MongoDB Reactive Streams Driver.
Essa mudança não é para excluir o Spring Data MongoDB. Em aplicações Spring, é possível conectar a nomeação de coleções do ReactiveMongoTemplate, MongoConverter, conversão personalizada e auditoria reativa ao adaptador MongoExecutionContext. O núcleo é mantido independente do framework, e apenas as aplicações necessárias continuam a usar as configurações existentes do Spring.
Ao mesmo tempo, a direção de não reimplementar funcionalidades que o MongoDB Driver já fornece dentro da DSL também se tornou mais clara. A agregação comum pode receber o estágio Bson diretamente, e para Search/Vector, há escape hatches como SearchOperator, VectorSearchQuery, driverOptions(...), stage(Bson). Essa é uma escolha para permitir que os usuários utilizem novas funcionalidades do Driver sem esperar pela próxima distribuição da DSL.
Filosofia de desenvolvimento
Não escondemos o MongoDB
Não era meu objetivo criar um ORM que transformasse as funcionalidades do MongoDB em conceitos de outros bancos de dados. Conectamos as consultas, agregações, transações, Atlas Search e Vector Search do MongoDB em fluxos mais curtos e fáceis de descobrir.
Adicionamos apenas as funcionalidades que realmente eram necessárias.
Não é uma biblioteca que foi implementada após o design da lista de funcionalidades. Foi necessário implementar a consulta em um projeto em operação, adicionar a funcionalidade de upsert e bulk, e incluir a pesquisa com Atlas Search e Vector Search.
Prioriza a produtividade em vez da perfeição estrutural.
Não afirma que uma classe central grande é a estrutura ideal. No início, era mais importante que a equipe lidasse rapidamente com operações do MongoDB de forma consistente, e ainda priorizamos o fluxo de uso real em vez da abstração em si.
Não recriamos o que o Driver já faz.
Se o MongoDB Java Driver tiver um builder tipado, usamos esse tipo sempre que possível. As combinações repetitivas que o DSL pode reduzir de forma valiosa são oferecidas como API de conveniência, mas não duplicamos a API do Driver apenas mudando o nome.
Deve ser possível descer para a API básica.
Não acreditamos que o DSL substitua todas as situações. Mantemos escape hatches como Bson, filtros/sort/operators/options do Driver, e customizadores de publisher, permitindo o uso direto do MongoDB Driver quando necessário.
Quatro fluxos abordados neste documento.
Consulta Mongo Geral.
Este é o fluxo inicial em que esta biblioteca começou. As condições e a execução são configuradas na ordem execute* → fields(...) → end() → find/findAll/count/delete/exists/atomicUpdate.
Agregação nativa do Driver.
Para controlar diretamente desde o primeiro estágio do pipeline ou usar a nova funcionalidade de agregação fornecida pelo Driver, usamos o fluxo aggregation().stage(Bson). Estágios como $score e $scoreFusion, que já são fornecidos pelo Driver, podem ser passados sem serem reimplementados pelo DSL.
Atlas Search
Este fluxo foi adicionado ao aplicar o Atlas Search ao projeto sem introduzir um serviço de busca separado. Após search(index), configuramos texto, composto, pontuação, destaque e tokens de sequência.
Pesquisa Vetorial
Este fluxo foi adicionado para conectar a pesquisa de embedding dentro do MongoDB. Configuramos opções de vetor de consulta ou embedding automatizado, ANN/ENN, pré/pós-filtro e opções de embedding aninhadas/array.
Modelo de execução da versão 1.0.0.
Separar consultas gerais de Search/Vector é para não ocultar as restrições reais do pipeline do MongoDB. $search e $vectorSearch têm restrições no primeiro estágio, e funcionalidades que exigem controle total do chamador no primeiro estágio podem ser tratadas com aggregation().
Escopo usado em projetos reais
• Passamos pares sem condições como null para montar condições de pesquisa dinâmicas por tela.
• Separando data e totalCount com PageStream, mantemos o fluxo de processamento em estado de publisher reativo.
• Criamos operações seguras contra execuções duplicadas com atomicUpdate().upsertOne().document().setOnInsert(...).
• Armazenamos dados coletados e dados de integração externa com bulk upsert com base em ID ou chave de trabalho.
• Retornamos os resultados de lookup e a contagem total em um único pipeline com executeLookupAndCount.
• Usamos o token de sequência do Atlas Search e a consulta de embedding automatizado do Vector Search.
• Os novos estágios de agregação fornecidos pelo Driver são conectados diretamente com aggregation().stage(Bson) ou stage(Bson) de Search/Vector.
• Se a atomicidade entre operações DSL for necessária, especificamos o escopo da transação ClientSession com getTxJob(...).