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.