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!
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 :).
- 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);
}
- 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())
);
}
}
- 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);
}
}
- 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
I faced something similar recently. Using Clazz as a substitute for my domain class, here is what I did:
long count = mongoTemplate.count(query, clazz.class);
query.with(pageable);
List<Clazz> found = mongoTemplate.find(query, clazz.class);
Page<Clazz> page = new PageImpl<T>(found, pageable, count);
I'm using spring-boot-starter-data-mongodb:1.5.5 and Query + MongoTemplate handles paging, sort of.
So, first I got the full count of all documents that matched the query. Then I did the search again using the peageable which searches again skipped and limited (not very efficient, I know). Back at the Controller, you can use the PagedResourcesAssembler's toResource(Page page) method to get that wrapped up with the paging Hyperlinks.
public ResponseEntity search(@Valid @RequestBody MyParams myParams, Pageable pageable, PagedResourcesAssembler assembler){
Page<Clazz> page= searchService.search(myParams, pageable, Clazz.class);
PagedResources<Clazz> resources = assembler.toResource(page);
return new ResponseEntity<>(resources, HttpStatus.OK);
}
As spring mongo repositories are not mature enough to handle all custom requirements (it was when we were using it back at 2016) I would suggest you to use mongotemplate which we're currently using and happy with all requirements. You can implement your own pagination logic. Let me explain.
In your REST calls you may request page
and pageSize
. And then you can simply calculate what to write to limit
and skip
values.
request.getPage() * request.getPageSize()
this will give you the skip value, and
request.getPageSize()
will give you the limit value. In detail, below implementation can be improved more:
Integer limit = request.getPageItemCount() == null ? 0 : request.getPageItemCount();
Integer skip = getSkipValue( request.getPage(), request.getPageItemCount() );
private Integer getSkipValue( Integer page, Integer pageItemCount ){
if( page == null || pageItemCount == null ){ return 0; }
int skipValue = ( page - 1 ) * pageItemCount;
if( skipValue < 0 ){ return 0; }
return skipValue;
}
Hope it helps.