Paginação e Consultas Dinâmicas com Spring Data MongoDB

Abordagem com MongoRepository

public interface MediaRepository extends MongoRepository<MediaItem, String> {
    MediaItem findByIdentifier(String identifier);
    Page<MediaItem> findByLevelAndSubject(Level level, Subject subject, Pageable pagination);
}

Limitações encontradas:

  • Como construir consultas com filtros dinâmicos?
  • Como projetar apenas campos específicos na resposta?

Projeção de campos com @Query

@Query(fields = "{'title':1, 'thumbnail':1, 'subject':1}")
Page<MediaItem> searchByCriteria(Criteria filter, Pageable pagination);

Construindo filtros dinâmicos com DBObject

BasicDBObject filterDoc = new BasicDBObject();
filterDoc.append("ownerId", new BasicDBObject("$gte", 2));

BasicDBObject projectionDoc = new BasicDBObject();
projectionDoc.append("ownerId", 1);
projectionDoc.append("displayName", 1);

Query dynamicQuery = new BasicQuery(filterDoc.toJson(), projectionDoc.toJson());

Exemplo prático com MongoTemplate

@Override
public PageResponse<MediaItem> fetchMediaPage(Pageable pagination, Long levelId, Long subjectId) {
    BasicDBObject conditions = new BasicDBObject();
    
    if (levelId != null && levelId != 0) {
        conditions.append("level.id", levelId);
    }
    if (subjectId != null && subjectId != 0) {
        conditions.append("subject.id", subjectId);
    }
    
    BasicDBObject projection = new BasicDBObject();
    projection.append("title", 1);
    projection.append("thumbnail", 1);
    projection.append("subject", 1);
    
    Query searchQuery = new BasicQuery(conditions.toJson(), projection.toJson());
    searchQuery.skip((int) pagination.getOffset()).limit(pagination.getPageSize());
    
    List<MediaItem> items = mongoTemplate.find(searchQuery, MediaItem.class);
    long total = mongoTemplate.count(searchQuery, MediaItem.class);
    
    return new PageResponse<>(items, pagination, total);
}

Consulta em propriedades aninhadas (objetos) pode não funcionar como epserado com essa abordagem.

Estratégia híbrida: MongoTemplate + Example

O MongoRepository permite projeção de campos via @Query, porém não suporta filtros dinâmicos. O MongoTemplate oferece flexibilidade na construção de Query com projeção dinâmica, mas consultas em propriedades de objetos são limitadas. A solução é combinar Example (para condições dinâmicas) com Query (para prrojeção de campos), usando o método addCriteria com Criteria.alike(example).

public interface MediaCustomOperations {
    Page<MediaItem> searchWithProjection(Example<MediaItem> example, DBObject projection, Pageable pagination);
}

public interface MediaRepository extends MongoRepository<MediaItem, String>, MediaCustomOperations {
    MediaItem findByIdentifier(String identifier);
}

public class MediaRepositoryImpl implements MediaCustomOperations {
    
    @Autowired
    private MongoTemplate mongoTemplate;
    
    @Override
    public Page<MediaItem> searchWithProjection(Example<MediaItem> example, DBObject projection, Pageable pagination) {
        Query searchQuery = new BasicQuery(new BasicDBObject(), projection);
        searchQuery.addCriteria(new Criteria().alike(example));
        searchQuery.with(pagination);
        
        List<MediaItem> results = mongoTemplate.find(searchQuery, example.getProbeType());
        
        return PageableExecutionUtils.getPage(results, pagination, 
            () -> mongoTemplate.count(searchQuery, example.getProbeType()));
    }
}

Referência de palavras-chave do Spring Data MongoDB

Palavra-chave Exemplo Equivalente MongoDB
After findByBirthdateAfter(Date date) {"birthdate" : {"$gt" : date}}
GreaterThan findByAgeGreaterThan(int age) {"age" : {"$gt" : age}}
GreaterThanEqual findByAgeGreaterThanEqual(int age) {"age" : {"$gte" : age}}
Before findByBirthdateBefore(Date date) {"birthdate" : {"$lt" : date}}
LessThan findByAgeLessThan(int age) {"age" : {"$lt" : age}}
LessThanEqual findByAgeLessThanEqual(int age) {"age" : {"$lte" : age}}
Between findByAgeBetween(int from, int to) {"age" : {"$gt" : from, "$lt" : to}}
In findByAgeIn(Collection ages) {"age" : {"$in" : [ages…]}}
NotIn findByAgeNotIn(Collection ages) {"age" : {"$nin" : [ages…]}}
IsNotNull, NotNull findByFirstnameNotNull() {"firstname" : {"$ne" : null}}
IsNull, Null findByFirstnameNull() {"firstname" : null}
Like, StartingWith, EndingWith findByFirstnameLike(String name) {"firstname" : name} (regex)
NotLike, IsNotLike findByFirstnameNotLike(String name) {"firstname" : { "$not" : name }} (regex)
Containing (String) findByFirstnameContaining(String name) {"firstname" : name} (regex)
NotContaining (String) findByFirstnameNotContaining(String name) {"firstname" : { "$not" : name}} (regex)
Containing (Collecsion) findByAddressesContaining(Address address) {"addresses" : { "$in" : address}}
NotContaining (Collection) findByAddressesNotContaining(Address address) {"addresses" : { "$not" : { "$in" : address}}}
Regex findByFirstnameRegex(String firstname) {"firstname" : {"$regex" : firstname}}
(Sem keyword) findByFirstname(String name) {"firstname" : name}
Not findByFirstnameNot(String name) {"firstname" : {"$ne" : name}}
Near findByLocationNear(Point point) {"location" : {"$near" : [x,y]}}
Near findByLocationNear(Point point, Distance max) {"location" : {"$near" : [x,y], "$maxDistance" : max}}
Near findByLocationNear(Point point, Distance min, Distance max) {"location" : {"$near" : [x,y], "$minDistance" : min, "$maxDistance" : max}}
Within findByLocationWithin(Circle circle) {"location" : {"$geoWithin" : {"$center" : [ [x, y], distance]}}}
Within findByLocationWithin(Box box) {"location" : {"$geoWithin" : {"$box" : [ [x1, y1], x2, y2]}}}
IsTrue, True findByActiveIsTrue() {"active" : true}
IsFalse, False findByActiveIsFalse() {"active" : false}
Exists findByLocationExists(boolean exists) {"location" : {"$exists" : exists}}

Tags: Spring Data MongoDB mongotemplate MongoRepository Pageable Example Matcher

Publicado em 7-31 17:29