Search code examples
mongodbspring-bootmongorepository

MongoRepository dynamic queries


I have the following problem. Lets say I have the following model object:

class Person {
    String id;
    String firstName;
    String lastName;
    Map<String, String> properties;
}

In the properties map, you can insert any kind of property, there are no constrains.

The above object is saved in a MongoDB which looks like this:

public interface PersonRepo extends MongoRepository<Person, String> {
}

When a person is saved into the repository the Map<String, String> properties is flatten up. As an example, if we have the following object:

Person: {
    id := 1;
    firstName := John,
    lastName  := Doe,
    properties := {
        age: 42
    }
}

the document saved in the MongoRepository will be the following:

Person: {
    id := 1;
    firstName := John,
    lastName  := Doe,
    age := 42
}

Now my problem is that I have to look for objects based on (for example), if they have a specific property or not. Lets say I want all Persons for which an age property has been defined. One important additional requirement is that I should return a paged result.

I've tried using the

findAll(Example<Person> example, Pageable pageable)

But this does not work for some reason. I suspect that it's the fact that my model object and the MongoDB Document have different structures.

I've also tried with the QueryDsl (here you have an example: http://www.baeldung.com/queries-in-spring-data-mongodb) but with no success either, and also to me this solution is not to elegant (having to mantain generated classes and alike. Also I have a feeling it will not work because of my Map<String, String> properties object member).

Another solution that came to my mind and would be elegant enough, is to have the following function:

@Query(value = "?0")
Page<Query> findByQuery(String query, Pageable pageable)

In this case I would be able to manually construct the query and I wouldn't have to hardcode the key by which I run the search. My question now is, how can set the query value to be exactly my first parameter? With the example showned above I get the following error

java.lang.ClassCastException: java.lang.String cannot be cast to com.mongodb.DBObject

One other solution would be to use mongoTemplate and query given some Criteria as in the following example:

    final Query query = new Query();
    query.addCriteria(Criteria.where("age").regex(".*"));

    mongoTemplate. find(query, Person.class);

The problem with this solution is that it returns a list of objects instead for a paged result. It cal also return a specific page if I add query.with(new PageRequest(3, 2)); but in this case I cannot manually construct the "paged" result because I do not know the total number of elements.

Do you have any other ideas that could help me?

Thanks in advance!


Solution

  • Bellow is the solution I've come up with. So just to recap, the problem that I had, was that I wan't able to execute a query given an Query object as input to have increased flexibility over the filtering criterias. The solution turned out to be quite simple, I just had to carefully read the documentation :).

    1. step

    Extends MongoRepository and add your custom functions:

    @NoRepositoryBean
    public interface ResourceRepository<T, I extends Serializable> extends MongoRepository<T, I> {
    
        Page<T> findAll(Query query, Pageable pageable);
    }
    
    1. step

    Create an implementation for this interface:

    public class ResourceRepositoryImpl<T, I extends Serializable> extends SimpleMongoRepository<T, I> implements ResourceRepository<T, I> {
    
        private MongoOperations mongoOperations;
        private MongoEntityInformation entityInformation;
    
        public ResourceRepositoryImpl(final MongoEntityInformation entityInformation, final MongoOperations mongoOperations) {
            super(entityInformation, mongoOperations);
    
            this.entityInformation = entityInformation;
            this.mongoOperations = mongoOperations;
        }
    
        @Override
        public Page<T> findAll(final Query query, final Pageable pageable) {
            Assert.notNull(query, "Query must not be null!");
    
            return new PageImpl<T>(
                    mongoOperations.find(query.with(pageable), entityInformation.getJavaType(), entityInformation.getCollectionName()),
                    pageable,
                    mongoOperations.count(query, entityInformation.getJavaType(), entityInformation.getCollectionName())
            );
        }
    }
    
    1. step

    Set your implementation as the default MongoRepository implementation:

    @EnableMongoRepositories(repositoryBaseClass = ResourceRepositoryImpl.class)
    public class MySpringApplication {
        public static void main(final String[] args) {
            SpringApplication.run(MySpringApplication.class, args);
        }
    }
    
    1. step

    Create a repository for your custom object:

    public interface CustomObjectRepo extends ResourceRepository<CustomObject, String> {
    }
    

    Now if you have multiple objects which you want to save in a mongo document store, it is enough to define an interface which extends your ResourceRepository (as seen in step 4), and you will have available the Page<T> findAll(Query query, Pageable pageable) custom query method. I have found this solution to be the most elegant of the solution I've tried.

    If you have any suggestions for improvements, please share them with the community.

    Regards, Cristian