Configuração e Manipulação Dinâmica do MongoDB com Spring Boot

Configuração do Projeto

Para estabelecer a comunicação entre uma aplicação Spring Boot e um banco de dados MongoDB, é necessário incluir o starter oficial do Spring Data no gerenciador de dependências do projeto.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

Propriedades de Conexão

A string de conexão (URI) deve ser definida no arquivo de configuração da aplicação (application.yml ou application.properties). O exemplo abaixo demonstra a conexão a um cluster com réplicas:

spring:
  data:
    mongodb:
      uri: mongodb://app_user:P%40ssw0rd!@node1:27017,node2:27017/production_db?authSource=admin&replicaSet=rs0&ssl=true

Atenção: Caso a senha do usuário contenha caracteres especiais (como @, :, /, !), é obrigatório aplicar a codificação URL (URL Encoding). A ausência dessa codificação resultará em falhas de autenticação, pois o driver interpretará os caracteres como delimitadores da URI.

Parâmetros da URI

  • replicaSet: Identificador do conjunto de réplicas. Essencial para arquiteturas de alta disponibilidade, mas dispensável em clusters fragmentados (sharded).
  • authSource: Define o banco de dados onde as credenciais do usuário estão armazenadas, geralmente o banco admin.
  • ssl: Habilita ou desabilita a criptografia no trânsito. O uso de true é altamente recomendado para ambientes produtivos.

Referência de Codificação URL

A tabela a seguir mapeia caracteres especiais comuns para suas respectivas representações codificadas:

Caractere Descrição Codificação
Espaço %20
! Exclamação %21
" Aspas duplas %22
# Hashtag %23
$ Cifrão %24
% Porcentagem %25
& E comercial %26
' Aspas simples %27
( Parêntese esquerdo %28
) Parêntese direito %29
* Asterisco %2A
+ Mais %2B
, Vírgula %2C
/ Barra %2F
: Dois pontos %3A
; Ponto e vírgula %3B
= Igual %3D
? Interrogação %3F
@ Arroba %40
[ Colchete esquerdo %5B
] Colchete direito %5D

Exemplo de implementação em Java para sanitização de credenciais:

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

public class CredentialUtils {
    public static String encodePassword(String rawPassword) {
        return URLEncoder.encode(rawPassword, StandardCharsets.UTF_8);
    }
    
    // Exemplo de uso: encodePassword("Mongo@2025#Cnpo!") retorna "Mongo%402025%23Cnpo%21"
}

Gerenciamento Dinâmico de Coleções e Documentos

Em cenários onde a estrutura dos dados (schema) é desconhecida ou altamente volátil, a manipulação direta via MongoTemplate utilizando a classe org.bson.Document é a abordagem mais flexível. O código abaixo ilustra uma classe de serviço refatorada para operações CRUD dinâmicas, incluindo agregações e paginação, sem depender de bibliotecas JSON externas.

import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class DynamicMongoOperations {

    private final MongoTemplate mongoTemplate;
    private static final String ID_FIELD = "_id";
    private static final String TIME_FIELD = "timestamp";

    public DynamicMongoOperations(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    public void ensureCollectionExists(String collectionName) {
        validateName(collectionName);
        if (!mongoTemplate.collectionExists(collectionName)) {
            mongoTemplate.createCollection(collectionName);
        }
    }

    public void deleteCollection(String collectionName) {
        validateName(collectionName);
        if (mongoTemplate.collectionExists(collectionName)) {
            mongoTemplate.dropCollection(collectionName);
        }
    }

    public void truncateCollectionData(String collectionName) {
        validateName(collectionName);
        if (mongoTemplate.collectionExists(collectionName)) {
            mongoTemplate.remove(new Query(), collectionName);
        }
    }

    public boolean renameAttribute(String collectionName, String oldKey, String newKey) {
        validateName(collectionName);
        Assert.hasText(oldKey, "O nome antigo do campo é obrigatório");
        Assert.hasText(newKey, "O novo nome do campo é obrigatório");
        
        Update update = new Update().rename(oldKey, newKey);
        return mongoTemplate.updateMulti(new Query(), update, collectionName).wasAcknowledged();
    }

    public boolean removeAttribute(String collectionName, String key) {
        validateName(collectionName);
        Assert.hasText(key, "O nome do campo é obrigatório");
        
        Update update = new Update().unset(key);
        return mongoTemplate.updateMulti(new Query(), update, collectionName).wasAcknowledged();
    }

    public boolean initializeAttribute(String collectionName, String key, Object defaultValue) {
        validateName(collectionName);
        Assert.hasText(key, "O nome do campo é obrigatório");
        
        Query checkQuery = new Query().addCriteria(Criteria.where(key).exists(false));
        Update update = new Update().setOnInsert(key, defaultValue != null ? defaultValue : 0.0);
        return mongoTemplate.updateMulti(checkQuery, update, collectionName).wasAcknowledged();
    }

    public void insertDocuments(String collectionName, List<document> documents) {
        validateName(collectionName);
        Assert.notEmpty(documents, "A lista de documentos não pode ser vazia");
        mongoTemplate.insert(documents, collectionName);
    }

    public void upsertDocument(String collectionName, Document document) {
        validateName(collectionName);
        Assert.notNull(document, "O documento não pode ser nulo");
        mongoTemplate.save(document, collectionName);
    }

    public boolean deleteByIds(String collectionName, List<string> ids) {
        validateName(collectionName);
        Assert.notEmpty(ids, "A lista de IDs não pode ser vazia");
        
        Query query = new Query(Criteria.where(ID_FIELD).in(ids));
        return mongoTemplate.remove(query, collectionName).wasAcknowledged();
    }

    public List<document> fetchRandomSample(String collectionName, int sampleSize, String... includeFields) {
        List<aggregationoperation> pipeline = new ArrayList<>();
        pipeline.add(Aggregation.sample(sampleSize));
        
        if (includeFields != null && includeFields.length > 0) {
            pipeline.add(Aggregation.project(includeFields));
        }
        
        Aggregation aggregation = Aggregation.newAggregation(pipeline);
        return mongoTemplate.aggregate(aggregation, collectionName, Document.class).getMappedResults();
    }

    public Map<string long=""> getTimeBoundaries(String collectionName) {
        validateName(collectionName);
        Map<string long=""> boundaries = new HashMap<>();
        
        List<aggregationoperation> pipeline = List.of(
            Aggregation.project(TIME_FIELD),
            Aggregation.group().min(TIME_FIELD).as("minTime").max(TIME_FIELD).as("maxTime")
        );
        
        Document result = mongoTemplate.aggregate(Aggregation.newAggregation(pipeline), collectionName, Document.class).getRawResults();
        
        if (result != null && result.getInteger("ok", 0) == 1) {
            List<document> mappedResults = result.getList("results", Document.class);
            if (mappedResults != null && !mappedResults.isEmpty()) {
                Document stats = mappedResults.get(0);
                boundaries.put("minTime", stats.getLong("minTime"));
                boundaries.put("maxTime", stats.getLong("maxTime"));
            }
        }
        
        return boundaries.isEmpty() ? Map.of("minTime", null, "maxTime", null) : boundaries;
    }

    public PaginatedResult<document> findPaginated(PaginationCriteria criteria) {
        Query query = buildQuery(criteria);
        long totalElements = mongoTemplate.count(query, criteria.collectionName());
        List<document> data = new ArrayList<>();
        
        if (totalElements > 0) {
            String sortField = StringUtils.hasText(criteria.sortBy()) ? criteria.sortBy() : TIME_FIELD;
            int skip = (criteria.pageNumber() - 1) * criteria.pageSize();
            
            query.with(Sort.by(sortField).ascending())
                 .skip(skip)
                 .limit(criteria.pageSize());
                 
            data = mongoTemplate.find(query, Document.class, criteria.collectionName());
        }
        
        return new PaginatedResult<>(totalElements, data);
    }

    public long countDocuments(FilterCriteria criteria) {
        return mongoTemplate.count(buildQuery(criteria), criteria.collectionName());
    }

    public List<document> findDocuments(PaginationCriteria criteria) {
        Query query = buildQuery(criteria);
        
        if (StringUtils.hasText(criteria.sortBy())) {
            query.with(Sort.by(criteria.sortBy()).ascending());
        }
        
        int skip = (criteria.pageNumber() - 1) * criteria.pageSize();
        query.skip(skip).limit(criteria.pageSize());
        
        return mongoTemplate.find(query, Document.class, criteria.collectionName());
    }

    public Document findSingleDocument(String collectionName) {
        validateName(collectionName);
        return mongoTemplate.findOne(new Query().limit(1), Document.class, collectionName);
    }

    public void cloneCollectionData(DataCloneRequest request) {
        long startTime = System.currentTimeMillis();
        log.info("Iniciando clonagem de [{}] para [{}]", request.sourceCollection(), request.targetCollection());
        
        truncateCollectionData(request.targetCollection());
        List<aggregationoperation> pipeline = new ArrayList<>();
        
        if (StringUtils.hasText(request.includeFields())) {
            pipeline.add(Aggregation.project(request.includeFields().split(",")));
        }
        if (StringUtils.hasText(request.beginTime())) {
            pipeline.add(Aggregation.match(Criteria.where(TIME_FIELD).gte(Long.parseLong(request.beginTime()))));
        }
        if (StringUtils.hasText(request.endTime())) {
            pipeline.add(Aggregation.match(Criteria.where(TIME_FIELD).lte(Long.parseLong(request.endTime()))));
        }
        
        pipeline.add(Aggregation.out(request.targetCollection()));
        
        Aggregation aggregation = Aggregation.newAggregation(pipeline);
        mongoTemplate.aggregateStream(aggregation, request.sourceCollection(), Document.class);
        
        log.info("Clonagem concluída em {} segundos", (System.currentTimeMillis() - startTime) / 1000);
    }

    private Query buildQuery(FilterCriteria criteria) {
        List<criteria> conditions = new ArrayList<>();
        
        if (StringUtils.hasText(criteria.beginTime())) {
            conditions.add(Criteria.where(TIME_FIELD).gte(Long.parseLong(criteria.beginTime())));
        }
        if (StringUtils.hasText(criteria.endTime())) {
            conditions.add(Criteria.where(TIME_FIELD).lte(Long.parseLong(criteria.endTime())));
        }
        
        Query query = conditions.isEmpty() ? new Query() : new Query(new Criteria().andOperator(conditions));
        
        if (StringUtils.hasText(criteria.includeFields())) {
            query.fields().include(criteria.includeFields().split(","));
        }
        if (StringUtils.hasText(criteria.excludeFields())) {
            query.fields().exclude(criteria.excludeFields().split(","));
        }
        
        return query;
    }

    private void validateName(String collectionName) {
        Assert.hasText(collectionName, "O nome da coleção é obrigatório");
    }

    // Registros auxiliares (DTOs)
    public record FilterCriteria(String collectionName, String beginTime, String endTime, String includeFields, String excludeFields) {}
    public record PaginationCriteria(String collectionName, int pageNumber, int pageSize, String sortBy, String beginTime, String endTime, String includeFields, String excludeFields) implements FilterCriteria {}
    public record DataCloneRequest(String sourceCollection, String targetCollection, String beginTime, String endTime, String includeFields) {}
    public record PaginatedResult<t>(long totalElements, List<t> data) {}
}</t></t></criteria></aggregationoperation></document></document></document></document></aggregationoperation></string></string></aggregationoperation></document></string></document>

Alternativa: Mapeamento Objeto-Documento (ODM)

A implementação baseada em Document demonstrada acima é ideal para schemas livres e coleções geradas dinamicamente. No entanto, quando a estrutura do banco de dados é previsível e imutável, a prática recomendada é adotar uma abordagem orientada a objetos utilizando as anotações do ecossistema Spring Data.

Ao criar classes POJO decoradas com @Document e @Field, o framwork gerencia automaitcamente a serialização, desserialização e o mapeamento de relações. Esse modelo assemelha-se ao comportamento de frameworks ORM (como Hibernate ou MyBatis-Plus) aplicados a bancos relacionais, proporcionando validação de tipos em tempo de compilação, consultas mais seguras via MongoRepository e um código de negócio significativamente mais limpo.

Tags: spring-boot mongodb mongotemplate java banco-de-dados-nosql

Publicado em 6-15 03:28 por Thomas