0

Hi I am having problems using Jongo to get a list of Addresses from my collection of Persons using the $unwind operator.

As you can see I defined the Person class as follows:

public class Person {
    @Id
    private long personId;
    private String name;
    private int age;
    private List<Address> addresses;
//getters and setters

and the Address class is defined like:

public class Address {
    private String houseNumber;
    private String road;
    private String town;
    private String postalCode;
//gettes and setters

The Query is pretty simple:

public List<Address> getAddressByPersonId(long id) {
    List<Address> list = persons.aggregate("{$project:{addresses:1}}")
                                    .and("{$match:{_id:#}}",id)
                                    .and("{$unwind: '$addresses'}")
                                    .as(Address.class);
    return list;
}

My collection:

> db.persons.find()
{ "name" : "Bob", "age" : 34, "addresses" : [   {       "houseNumber" : "12",
"road" : "High Street",         "town" : "Small Town",  "postalCode" : "BC2 3DE"
 },     {       "houseNumber" : "12",   "road" : "High Street",         "town" :
 "Small Town",  "postalCode" : "BC2 3DE" } ], "_id" : NumberLong(1) }
>

I have a JUnit test for this as well:

    @Before
    public void setUp(){
        service = new PersonServiceImpl();

        //store a person into the database (manually) before the tests
        MongoCollection persons = Database.getInstance().getCollection(CollectionNames.PERSONS);
        //create a new person
        Person p = new Person();
        p.setPersonId(1L);
        p.setName("Bob");
        p.setAge(34);
        //create two addresses for this person
        Address a = new Address();


        a.setHouseNumber("33");
        a.setRoad("Fake Road");
        a.setPostalCode("AB1 2CD");
        a.setTown("Big Town");
        p.addAddress(a);

        a.setHouseNumber("12");
        a.setRoad("High Street");
        a.setPostalCode("BC2 3DE");
        a.setTown("Small Town");
        p.addAddress(a);

        persons.save(p);
    }

    @After
    public void tearDown(){
        service = null;
    }

    @Test
    public void getAddressesByPersonIdTest(){

        List<Address> list = service.getAddressByPersonId(1L);
        for (Address item : list){
            item.print();
        }

        Assert.assertTrue(list.size() > 0);
    }
}

Which outputs

### Address: null null, null, null 
### Address: null null, null, null 

I don't understand very well what the problem is. Apparently the test does not fail (so list.size() is bigger than zero...but prints null). The print() method is tested and works.

I would like to get the two Bob's addresses , however the query returns null objects. Am I missing something? Should I use the $unwind differently? Please suggest

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
nuvio
  • 2,555
  • 4
  • 32
  • 58

2 Answers2

1

Aggregation Framework returns the exact same format as goes into it - after the unwind the only difference is how many addresses are embedded under the addresses field. In your case you get back two documents which are:

{ "_id": 1,
  "name" : "Bob", 
  "age" : 34, 
  "addresses" : { "houseNumber" : "12", 
                  "road" : "High Street",
                  "town" : "Small Town",
                  "postalCode" : "BC2 3DE"
  }
}

This is not converting to Address class very effectively because as you can see, Address object is the value of addresses field, and not the top level object.

What is not clear is why you need to aggregate at all - you are getting a list of addresses for a person. When you do a simple find() on the person by Id, the field "addresses" is already a List of Address objects.

In MongoDB find() has the option to return just selected fields, or exclude named fields (analogous to SQL's SELECT * not being as efficient as SELECT col1,col2) so if you do the Java equivalent of db.persons.find({filter-condition}, {"_id":0, "addresses":1}) you will get back a document with only a list of addresses.

Asya Kamsky
  • 41,784
  • 5
  • 109
  • 133
  • Yes I understand that, my point is that if I have a Person with a field 'addresses' a field 'purchases-history' a field 'search-history' a field 'credit-cards' (just as an example)... Why would I need to map the entire top level object when I only need the addresses? Pheraps my approach is completly wrong, but @Shad's answer solve my question. Thanks – nuvio Nov 17 '13 at 12:09
  • 1
    I'm not sure if you are aware of it, but you do not have to receive the entire document when you query MongoDB. find() takes two arguments, the first is a "filter" - like a where clause, the second is a list of fields either to include or to exclude. All you need is to specify that you want back _{ "_id":0, "addresses":1}_ and you get back _just_ a list of addresses. – Asya Kamsky Nov 17 '13 at 19:25
1

I'd like to add some extra information to solve the problem using Jongo.

I wanted to map Mongo results straight to a list of Addresses object, avoiding unnecessary instantiation of Person objects. Imagine my Person class has these extra fields:

...
List<Address> addresses; 
List<Purchases> purchases; 
List<Payments> creditcards;
List<Product> watchlist;
//etc
...

However we only need to retrieve 'addresses'. So the first approach could be:

Solution 1)

public List<Address> getAddressByPersonId(long id) {
        List<Address> list = persons.aggregate("{$match: {_id: #}}", id)
                .and("{$project: {addresses: 1, _id: 0}}")
                .and("{$unwind: '$addresses'}")
                .and("{$project: {houseNumber:'$addresses.houseNumber', road:'$addresses.road', town:'$addresses.town', postalCode:'$addresses.postalCode'}}")
                .as(Address.class);
        return list;
    }

Pros: I only need a list of Addresses, less memory allocation. Cons: I need to $project every field of the Address class otherwise this will not be mapped in Jongo (null). As @Asya Kamsky pointed out, this is not scalable solution.

Solution 2)

public List<Address> getAddressByPersonId(long id) {
        List<Person> list = persons.aggregate("{$match:{_id:#}}",id).and("{$project: {addresses: 1, _id: 0}}")
                .as(Person.class);

        return list.get(0).getAddresses();
    }

Pros: I don't need to specify which fields of the adddress list we want. So it is a scalable solution in the case I want to add an extra field. Cons: I need to allocate the whole Person object which can translate into considerable overhead when dealing with complex objects and loads of records in similar scenarios.

If you have any other working (Jongo) solutions please share it.

nuvio
  • 2,555
  • 4
  • 32
  • 58