Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot Data and MongoDB - Filter Subdocument Array Query

I am attempting to use Spring to Query a Mongo repository and filter an array subdocument. I have referenced how to filter array in subdocument with mongodb, but was wondering if there was a more appropriate or java structured method to do so using Spring.

I am currently using the shorthand repository interface notation, but I am getting back the complete document with the array not filtered.

PersonRepository.java

@Repository
public interface PersonRepository extends MongoRepository <Person, String> {
    List<Person> findByAddressZipCode(@Param("zip") int zip);
}

Person.java

@Document
public class Person {
    @Id
    private String id;

    private String firstName;
    private String lastName;
    private List<Address> address;
}

Address.java

public class Address {
    private int zip;
}

Sample Input

{
 "firstName":"George",
 "lastName":"Washington",
 "address":[{
     "zip":"12345"
  },{
     "zip":"98765"
  },{
     "zip":"12345"
  }]
}

Expected Output

{
 "firstName":"George",
 "lastName":"Washington",
 "address":[{
     "zip":"12345"
  },{
     "zip":"12345"
  }]
}

Actual Output

{
 "firstName":"George",
 "lastName":"Washington",
 "address":[{
     "zip":"12345"
  },{
     "zip":"98765"
  },{
     "zip":"12345"
  }]
}
like image 604
Eagles5iveBC Avatar asked Jan 12 '16 19:01

Eagles5iveBC


1 Answers

Well, In Spring Data such kind of queries is not trivial.

Bad news:
Spring Data Repository does not have solution for MongoDB Aggregation. So, you cannot implement in MongoRepository any method to do so, like aggregateBy...

Good news:
Spring Data provides MongoTemplate class which allows you to execute complex queries, like you would do in standard MongoDB shell.

So, as you just want to exclude subdocument that does not match some condition, we need to define the aggregate pipelines.

I assume:

zip codes are Numeric (In your example is string)
And, to exclude subdocument, we filter by `zip`
There is no any other filter

MongoDB aggregation would be:

db.person.aggregate([
    {$unwind: "$address"},
    {$match: {"address.zip": 12345}},
    {$group: { _id: { "firstName":"$firstName", "lastName":"$lastName", _id:"$_id" }, address: { $push: "$address" } } },
    {$project: {_id:0, "firstName":"$_id.firstName", "lastName":"$_id.lastName", "address": "$address"}}
])

If all filters success, we got:

[ 
    {
        "address" : [ 
            {
                "zip" : 12345
            }, 
            {
                "zip" : 12345
            }
        ],
        "firstName" : "George",
        "lastName" : "Washington"
    }
]


Now, in Spring Data way, you need add some changes in your project:

First, find your mongo-config.xml where you need to add:

<!-- Define the mongoDbFactory with your database Name  -->
<mongo:db-factory uri="mongodb://user:pass@localhost:27017/db"/>

<!-- Define the MongoTemplate  -->
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
    <constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
</bean>

MongoTemplate is the central class of the Spring’s MongoDB support providing feature sets to interact with the database. The template ... provides a mapping between your domain objects and MongoDB documents. More info

Second, in your @Service class, add following code to be loaded in @PostConstruct

@Autowired
private MongoOperations mongoOperations;

...

public List<Person> findByAddressZipCode(int zip) {

    List<AggregationOperation> list = new ArrayList<AggregationOperation>();
    list.add(Aggregation.unwind("address"));
    list.add(Aggregation.match(Criteria.where("address.zip").is(zip)));
    list.add(Aggregation.group("firstName", "lastName").push("address").as("address"));
    list.add(Aggregation.project("firstName", "lastName", "address"));
    TypedAggregation<Person> agg = Aggregation.newAggregation(Person.class, list);
    return mongoOperations.aggregate(agg, Person.class, Person.class).getMappedResults();
}

Note: Both, Person and Address should have default empty constructor!

like image 185
Valijon Avatar answered Oct 02 '22 12:10

Valijon