django-mongodb-engine supports MongoDB's embedded objects.
Using ~djangotoolbox-fields.EmbeddedModelField
, you can embed any Django model in other models:
from djangotoobox.fields import EmbeddedModelField
class ModelA(models.Model):
b = EmbeddedModelField(ModelB) # or 'B' (in quotes)
With ~djangotoolbox-fields.EmbeddedModelField
, which was originally written for django-mongodb-engine and then merged into djangotoolbox, you can embed any Django model into another model. Example:
from django.db import models
from djangotoolbox import fields
class Customer(models.Model):
name = models.CharField()
address = fields.EmbeddedModelField('Address')
class Address(models.Model):
city = models.CharField()
street = models.CharField()
Similar to relation definitions, the model passed to ~djangotoolbox-fields.EmbeddedModelField
can either be the class name (Address
) or a string containing the class name ('Address'
).
Let's add a customer to the database. :
Customer.objects.create(
name='John',
address=Address(city='London', street='Some street')
)
The resulting data record can be inspected with the MongoDB console:
/* db.docs_customer.findOne() */
{
"_id" : ObjectId("4ce..."),
"name" : "John",
"address" : {
"city" : "London",
"street" : "Some street",
}
}
As you see, the address is stored as a nested JSON/BSON object.
It's cool to use Embedded Objects in a list instead of relations:
from djangotoobox.fields import EmbeddedModelField, ListField
class ModelA(models.Model):
b = ListField(EmbeddedModelField(ModelB))
If used in a list or dict or similar, Embedded Objects can be a good or even better alternative to relations.
Imagine you're a music store and you have those models:
from djangotoolbox.fields import EmbeddedModelField, ListField
class Musician(models.Model):
name = models.CharField()
albums = ListField(EmbeddedModelField('Album'))
class Album(models.Model):
title = models.CharField()
year = models.IntegerField()
tracks = ListField(EmbeddedModelField('Track'))
class Track(models.Model):
title = models.CharField()
length = models.IntegerField()
Now let's add a data record :
dimmu= Musician.objects.create(name='Dimmu Borgir')
in_sorte_diaboli = Album(
title='In Sorte Diaboli', year=2007,
tracks=[Track(title='The Serpentine Offering', length=309),
Track(title='The Chosen Legacy', length=257),
Track(title='The Conspiracy Unfolds', length=324)]
)
dimmu.albums.append(in_sorte_diaboli)
dimmu.save()
and have a look at what BSON was generated by django-mongodb-engine:
/* > db.docs_musician.findOne() */
{
"_id" : ObjectId("4cee2549e4721c3b2c000001"),
"name" : "Dimmu Borgir",
"albums" : [
{
"title" : "In Sorte Diaboli",
"year" : 2007,
"tracks" : [
{
"length" : 309,
"title" : "The Serpentine Offering"
},
{
"length" : 257,
"title" : "The Chosen Legacy"
},
{
"length" : 324,
"title" : "The Conspiracy Unfolds"
}
]
}
]
}
Neat, isn't it?
because there's no difference between an insert and update in MongoDB.
The model structure used above makes queries down the data tree easy, e.g. get me all albums by musician xy or get me all albums whose title starts with an 'a'. You could even emulate queries like get me all tracks of musician xy:
sum((album.tracks for album in Musician.objects.get(name=xy).album), [])
However, queries that would normally require reverse-JOINs are not possible. For example, get me all albums from 2005 with the related musicians simply is impossible on non-relational databases as such a query would require JOINs. Emulation of such queries is possible but very expensive because you have to crawl through the whole collection:
# VERY expensive and slow query. Don't do this!
result = []
for musician in Musician.objects.all():
musician.albums = filter(lambda album:album.year == 2005, musician.albums)
if musician.albums:
result.append(musician)
What's the point?
Consider your queries when designing your models.
objects.filter(field=A('subfield', 'value'))
translates to the Mongo query
.find( {'field.subfield' : 'value'} )
django-mongodb-engine has full support for queries and updates that reach into subobjects using the ~django_mongodb_engine.query.A
query helper.
For example, find any customer whose habitat is London:
>>> from django_mongodb_engine.query import A
>>> Customer.objects.create(name='Bob', address=Address(city='NY'))
>>> Customer.objects.create(name='Ann', address=Address(city='London'))
>>> Customer.objects.filter(address=A('city', 'London'))
[<Customer 'Ann'>]
If you enable query debugging <query-debugging>
, you can see what this query translates to:
DEBUG [...] .find {'address.city': 'London'} [...]
If you need to match multiple filters on the same embedded field, chain ~django.db.query.QuerySet.filter
calls:
Customer.objects.filter(address=A('city', 'London')) \
.filter(address=A('street', 'Some street'))
This translates to:
.find( {'address.city': 'London', 'address.street': 'Some street'} )
objects.filter(foo='bar').update(field=A('subfield', 'new-value'))
translates to the Mongo query
.update( {foo: 'bar'}, {'$set' : {'field.subfield' : 'new-value'}} )
Updating works similar to this, e.g. John moved to Houston:
Customer.objects.filter(name='John').update(addresss=A('city', 'Houston'))
Translates to:
.update( {'name': 'John'}, {'$set': {'address.city': 'Houston'}} )
Of course, ~django_mongodb_engine.query.A
can be used for both filtering and updating at the same time; for example, for moving all customers from New York to Canasas:
Customer.objects.filter(address=A('city', 'NY')) \
.update(address=A('city', 'Cansas'))
Which translates to:
.update( {'address.city': 'NY'}, {'$set': {'address.city': 'Cansas'}} )