Mongoengine: Can not query on embedded documents for greater than

Created on 4 Jun 2016  路  2Comments  路  Source: MongoEngine/mongoengine

I have a database model:

class Wallet(EmbeddedDocument):
    is_active = BooleanField(default=True)
    balance = IntField(default=0, min_value=0)

class User(Document):
    email = EmailField(required=True, unique=True)
    mobile = StringField(required=True, unique=True, min_length=10, max_length=10)
    first_name = StringField(required=True)
    last_name = StringField()
    default_address_id = ObjectIdField()
    wallet = EmbeddedDocumentField(Wallet, default=Wallet())

I want to query all the Users who have a wallet balance of more than 10,000. This is the query I wrote
User.objects.filter(wallet__balance__gt=0)
However it returns an empty list, whereas, the query
User.objects.filter(wallet__balance=11500) returns multiple objects.
[<User: User object>, <User: User object>, <User: User object>, <User: User object>]

This does not work for less than queries as well.
Is there something which I am doing wrong?

Awaiting Response Bug

Most helpful comment

First off, there's something _insanely dangerous_ in your example that you might not realize.

wallet = EmbeddedDocumentField(Wallet, default=Wallet())

This does not do what you think it does. Very specifically, every new instance of User constructed will _share a single Wallet instance_.

from mongoengine import Document, EmbeddedDocument, StringField, EmbeddedDocumentField, IntField

class Foo(EmbeddedDocument):
    baz = StringField()
    amount = IntField(default=0)

class Bar(Document):
    foo = EmbeddedDocumentField(Foo, default=Foo())

a = Bar()
a.foo.baz = "Hi."

b = Bar()
print(b.foo.baz)
# Hi.

a.foo.amount = 27
print(b.foo.amount)
# 27

This is somewhat separate from the overall issue, of course, but a common pitfall for users new to Python. This also happens if you instantiate something as a default argument to a function, take the following example:

def appendto(value, l=[]):
  l.append(value)
  return l

print(appendto(27))
# [27]
print(appendto(42))
# [27, 42]

MongoEngine allows you to define a callback to populate the default. Since classes are themselves callable (like functions) and return a value (an instance), just drop the () in your default definition and a new instance will be freshly created for each new instance of the containing document class.

I'm testing on 0.10.5 and it seems to work, however:

a.save()

Bar.objects(foo__amount=27)  # [<Bar: Bar object>]
Bar.objects(foo__amount__gt=42)  # []
Bar.objects(foo__amount__lt=42)  # [<Bar: Bar object>]
Bar.objects(foo__amount__gt=0)  # [<Bar: Bar object>]

Updating to the latest (0.10.6) also seems to work.

All 2 comments

First off, there's something _insanely dangerous_ in your example that you might not realize.

wallet = EmbeddedDocumentField(Wallet, default=Wallet())

This does not do what you think it does. Very specifically, every new instance of User constructed will _share a single Wallet instance_.

from mongoengine import Document, EmbeddedDocument, StringField, EmbeddedDocumentField, IntField

class Foo(EmbeddedDocument):
    baz = StringField()
    amount = IntField(default=0)

class Bar(Document):
    foo = EmbeddedDocumentField(Foo, default=Foo())

a = Bar()
a.foo.baz = "Hi."

b = Bar()
print(b.foo.baz)
# Hi.

a.foo.amount = 27
print(b.foo.amount)
# 27

This is somewhat separate from the overall issue, of course, but a common pitfall for users new to Python. This also happens if you instantiate something as a default argument to a function, take the following example:

def appendto(value, l=[]):
  l.append(value)
  return l

print(appendto(27))
# [27]
print(appendto(42))
# [27, 42]

MongoEngine allows you to define a callback to populate the default. Since classes are themselves callable (like functions) and return a value (an instance), just drop the () in your default definition and a new instance will be freshly created for each new instance of the containing document class.

I'm testing on 0.10.5 and it seems to work, however:

a.save()

Bar.objects(foo__amount=27)  # [<Bar: Bar object>]
Bar.objects(foo__amount__gt=42)  # []
Bar.objects(foo__amount__lt=42)  # [<Bar: Bar object>]
Bar.objects(foo__amount__gt=0)  # [<Bar: Bar object>]

Updating to the latest (0.10.6) also seems to work.

@amcgregor Thanks for the detailed explanation.
Yes it is working, I probably was making an incorrect query. I even wrote a pymongo implementation to handle this :(
Thanks for the heads-up on the default value , I am changing that in the models :)

Was this page helpful?
0 / 5 - 0 ratings