References and Relationships
A ReferenceField is how one document points at another in a different collection: it stores a bare ObjectId and provides dereference() to load the target. References are the "many to one, or many to many, with a life of their own" relationship - a post pointing at its author, an order pointing at a customer - and they are the MongoDB answer to a foreign key, with the important caveat that nothing in the database enforces them.
Declaring a Reference
from mongoengine import Document, StringField, ReferenceField, ListField
class User(Document):
username = StringField(required=True, unique=True)
class Post(Document):
title = StringField(required=True)
author = ReferenceField(User, required=True)
co_authors = ListField(ReferenceField(User), default=list)
meta = {"indexes": ["author"]}
The stored value is an ObjectId, not the target document: a query on posts fetches the reference in one trip, and the author is loaded only when you ask for it. required=True applies to the presence of the ObjectId. The meta index on author is what makes "all posts by this user" fast - the reference field is indexable exactly like any other field.
Setting and Dereferencing
ada = User(username="ada").save()
post = Post(title="First principles", author=ada) # accept a document
post2 = Post(title="Second post", author=ada.id) # ... or a bare ObjectId
post.save()
# the loaded post holds the reference; load the target explicitly
fetched = Post.objects.get(id=post.id)
author = fetched.author.dereference()
print(author.username) # 'ada'
# co_authors: dereference each, or in bulk
authors = [u.dereference() for u in fetched.co_authors]
loaded = User.objects.in_(authors) # bulk-load in one query
dereference() performs the second query and returns the target document (or None if the ObjectId points at nothing). The bulk pattern - collect the ObjectIds, then one objects.in_(...) - is how you avoid an N+1 when a result set references many targets: one query for all authors instead of one per post.
Finding What References a Document
ada_posts = Post.objects(author=ada) # forward: easy, indexed
# reverse: "all posts that mention ada as a co-author"
ada_posts2 = Post.objects(co_authors=ada)
# and the general case: any document type that references her
# (there is no single reverse-reference API - you query per field)
MongoDB has no automatic reverse-lookup: "everything pointing at X" is a query per referencing field, which is why the forward reference plus its index is the design unit. When you need the reverse view often (a user's activity feed from many document types), either query the known referencing collections or maintain a denormalized list on the target.
Generic References
from mongoengine import GenericReferenceField, GenericRelField
class Comment(Document):
text = StringField()
# points at ANY document type - post, page, user...
parent = GenericReferenceField()
comment = Comment(text="nice work", parent=post) # a Post instance
comment.save()
target = comment.parent.dereference() # the actual Post
A GenericReferenceField stores the ObjectId plus the target collection name, so one field can point at any document type - the pattern for comments, mentions, and audit trails. The trade: you cannot index a generic reference by target type the way you can a typed ReferenceField, and reverse lookups mean querying each possible collection. Use it for polymorphic edges, not for your core relational spine.
Referential Integrity Is Your Job
- Deleting a
Userdoes not delete her posts or clear theirauthor- the ObjectIds remain anddereference()returnsNone. - Decide the policy per relationship: cascade (delete children first), reassign, or soft-delete the parent so references stay valid.
- Signals (the next chapter) are the standard hook:
pre_deleteonUserthat removes or re-points her posts before the delete runs.
Summary
Model cross-collection relationships with ReferenceField: it stores an ObjectId, loads the target on demand with dereference(), and is indexable like any field. Bulk-load many targets with one in_ query to avoid N+1, use GenericReferenceField only for truly polymorphic edges, and remember that nothing enforces referential integrity - the cascade, reassignment, or soft-delete policy for each relationship is a design decision you make, usually enforced with signals.