Mongodb(via MongoEngine) join query with aggregate
Since Mongodb 3.2 and MongoEngine 0.9, we can use $aggregate command to perform join queries on multiple collections in a database. This post would be a simple tutorial for join queries on Mongodb(via MongoEngine in Python) with examples.
Models Setup
Let's consider models defined as below:
1import random
2import mongoengine
3
4
5class User(mongoengine.Document):
6 meta = {"indexes": ['rnd']}
7 name = mongoengine.StringField()
8 rnd = mongoengine.FloatField(default=random.random)
9
10
11class Group(mongoengine.Document):
12 meta = {"indexes": ['rnd']}
13 name = mongoengine.StringField()
14 rnd = mongoengine.FloatField(default=random.random)
15
16
17class Relation(mongoengine.Document):
18 meta = {"indexes": ['user_id', 'group_id']}
19 user_id = mongoengine.ObjectIdField()
20 group_id = mongoengine.ObjectIdField()
Note:
- User and Group documents will be generated randomly.
- rnd fields in User and Group, are used for efficiently pick a random document.
- Relation documents are matched by randomly picking (User, Group) pairs.
Join Queries
Based on the Mongodb official document, aggregation enables pipelining several operators on the original data from a collection. From MongoEngine you may use aggregation via
1cursor = SomeDocumentClass.objects.aggregate(*pipelines)
where pipelines is a list of stages defined by a dict(the keys of the dict can be found via the document). And let's run a few examples.
Join User and Relation
The join is powered by the $lookup operator.
1curosr = User.objects.aggregate(*[
2 {
3 '$lookup': {
4 'from': Relation._get_collection_name(),
5 'localField': '_id',
6 'foreignField': 'user_id',
7 'as': 'relation'}
8 }])
A object from the result set would be like
1{
2 '_id': ObjectId('58725790958bee0e467d12fe'),
3 'rnd': 0.4821115717624539,
4 'relation': [
5 {
6 'user_id': ObjectId('58725790958bee0e467d12fe'),
7 '_id': ObjectId('58725790958bee0e467d12ff'),
8 'group_id': ObjectId('58725790958bee0e467d12e4')
9 }
10 ],
11 'name': 'user_sqkXPqverW'}
Use $unwind to deal with list
You may have noticed from above results, the relation in a result object was a list since there may be one-to-many mapping. The $unwind will let you expand the list. And remember that aggregate is an pipeline, so we just append $unwind operators behind.
1curosr = User.objects.aggregate(*[
2 {
3 '$lookup': {
4 'from': Relation._get_collection_name(),
5 'localField': '_id',
6 'foreignField': 'user_id',
7 'as': 'relation'},
8 '$unwind': {
9 '$unwind': "$relation"}
10 }])
And from the results, you will notice that relation became a dict:
1{
2 '_id': ObjectId('58725790958bee0e467d12fe'),
3 'rnd': 0.4821115717624539,
4 'relation': {
5 'user_id': ObjectId('58725790958bee0e467d12fe'),
6 '_id': ObjectId('58725790958bee0e467d12ff'),
7 'group_id': ObjectId('58725790958bee0e467d12e4')
8 },
9 'name': 'user_sqkXPqverW'}
Join more collections: User, Relation and Group
Join more collections are pretty simple, you can just append more $lookup operators in the pipeline.
1cursor = User.objects.aggregate(*[
2 {'$lookup': {'from': Relation._get_collection_name(),
3 'localField': '_id',
4 'foreignField': 'user_id',
5 'as': 'relation'}},
6 {'$unwind': "$relation"},
7 {'$lookup': {'from': Group._get_collection_name(),
8 'localField': 'relation.group_id',
9 'foreignField': '_id',
10 'as': 'group'}},
11 {'$unwind': "$group"},
12 ])
And in result objects, there would be group object:
1{
2 'group': {
3 '_id': ObjectId('58725790958bee0e467d12e4'),
4 'rnd': 0.9648027926390293,
5 'name': 'group_xgsYceERw'
6 },
7 '_id': ObjectId('58725790958bee0e467d12fe'),
8 'rnd': 0.4821115717624539,
9 'relation': {
10 'user_id': ObjectId('58725790958bee0e467d12fe'),
11 '_id': ObjectId('58725790958bee0e467d12ff'),
12 'group_id': ObjectId('58725790958bee0e467d12e4')},
13 'name': 'user_sqkXPqverW'
14 }
Several Notes
ObjectId Field
You should use ObjectId to perform join with other collection's _id field(which is also an ObjectId), Mongodb can not do type conversion from ObjectId to string(or vice vesa) in a aggregation query.
There are several issues on Mongodb official jira addressing the problem(SERVER-11400, SERVER-22781, SERVER-24947).