xeraa
xeraa

Reputation: 10859

Morphia: Search for null values in a list of embedded objects

I have a list of embedded entities:

@Embedded
private List<EmbeddedEntity> embedded = new ArrayList<EmbeddedEntity>();

In this list, I want to search for any embedded entity which has a specific attribute (let's call it foo), but not another one (bar). So foo should be non-null and bar null in Java / inexistant in MongoDB.

I tried the following code (I do have the UUID of the Entity containing the list):

Query<Entity> query = mongoDataStore.find(Entity.class).field("uuid").equal(uuid)
    .field("embedded.foo").exists()
    .field("embedded.bar").doesNotExist();

This works correctly if the list is empty or has a single entry (where foo has a value and bar does not yet exist). But as soon as any bar attribute has a value, the query returns the wrong result. So I'm looking for a query, which iterates through all the embedded entities and fires on any missing bar. Is that possible?

Example data:

// the query does not pick up the entity as it doesn't have a foo -
// that's what I want
{ uuid: "...", [ { embedded: } ] }

// the query picks up the entity as there is a foo but no bar -
// that's what I want
{ uuid: "...", [ { embedded: { foo: date } } ] }

// the query does not pick up the entity - that's not what I want
// as one foo has a value and its bar doesn't
{ uuid: "...", [ { embedded: { foo: date, bar: date } },
                 { embedded: { foo: date } }
               ] }

PS: I get the same result with .field("embedded.bar").hasThisOne(null).

PPS: Manually iterating through the list elements is not really an option, as I want to use the query for an update operation.

PPS: I think this is a bug in Morphia - see my answer below (https://stackoverflow.com/a/9705175/573153) for a workaround

Upvotes: 1

Views: 4534

Answers (2)

Remon van Vliet
Remon van Vliet

Reputation: 18595

I think you have a conceptual problem with your schema. Remember that you always query for top level documents. The reason your query isn't working is because you're asking it to return top level documents where at least one of the elements has foo and at least one of the elements does not have a bar value. Note that both conditions need not apply on the same array element.

You can do what you want in MongoDB using a $elemMatch :

find({embedded:{$elemMatch:{foo:{$exists:true}, bar:{$exists:false}}}}) as demonstrated here :

> db.test.save({embedded:[]})
> db.test.save({embedded:[{foo:1}]})
> db.test.save({embedded:[{bar:1}]})
> db.test.save({embedded:[{foo:1, bar:1}]})
> db.test.find({embedded:{$elemMatch:{foo:{$exists:true}, bar:{$exists:false}}}})
{ "_id" : ObjectId("4f60c4d56fa40267a11d2f2c"), "embedded" : [ { "foo" : 1 } ] }

If "null" is a valid value for bar you can simply change it to :

> db.test.save({embedded:[{foo:1, bar:null}]})
> db.test.find({embedded:{$elemMatch:{foo:{$exists:true}, $or:[{bar:{$exists:false}}, {bar:null}]}}})
{ "_id" : ObjectId("4f60c4d56fa40267a11d2f2c"), "embedded" : [ { "foo" : 1 } ] }
{ "_id" : ObjectId("4f60c52a6fa40267a11d2f30"), "embedded" : [ { "foo" : 1, "bar" : null } ] }

Now, in Morphia the $elemMatch is wrapped by the FieldEnd method "hasThisElement". I'm not that familiar with Morphia (I wrote and use my own mapper) but this should take a DBObject with the above clause as it's value and that should result in what you need to do.

But again, this will return the top level documents that have elements in their embedded array that match these criteria. If you only want to return the matching elements you may have to turn your embedded structure in a top-level collection. It will be sufficient if your update only involves modifying the matching element(s) through the $ positional operator though :

db.test.update(
    {embedded:{$elemMatch:{foo:{$exists:true}, $or:[{bar:{$exists:false}}, {bar:null}]}}},
    {$set:{'embedded.$.bar':"yay!"}}
)

Upvotes: 3

xeraa
xeraa

Reputation: 10859

I've found a workaround. While I don't seem to be able to query on null, I can query for a specific value.

In my case, the bar field is a Date. So I can initialize the entity with private Date bar = new Date(0) - this is clearly an invalid date in my case, which is never used. So the query then looks like this:

Query<Entity> query = mongoDataStore
    .find(Entity.class)
    .field("uuid").equal(uuid)
    .field("embedded.foo").exists()
    .field("embedded.bar").hasThisOne(new Date(0));

And in case anyone needs it, here is the update operation (you need to disable the validation as .$. will otherwise raise an error):

UpdateOperations<Entity> update = mongoDataStore
    .createUpdateOperations(Entity.class)
    .disableValidation()
    .set("embedded.$.bar", new Date());

mongoDataStore.update(query, update);

Upvotes: 4

Related Questions