Updating Documents
Updating has two paths with different properties. Loading a document, changing it, and calling save() runs validation and gives you the full document in hand - right for changes that need context. Queryset update() methods perform the write server-side in a single atomic operation, never materializing the document - right for counters, flags, and bulk changes where touching every row is wasted work. Choosing between them is the core skill of this chapter.
The Save Path (Read-Modify-Write)
post = Post.objects.get(slug="hello")
post.views += 1
post.title = "New title"
post.save() # validates the whole document, then writes it
Three costs: a read and a write (two round trips), validation of the full document, and a race if two processes modify the same document concurrently (last writer wins). All three are acceptable - and validation is a feature - when the change depends on the document's current state or when you are already holding the instance.
Atomic Queryset Updates
from mongoengine import Document, IntField, ListField
class Post(Document):
views = IntField(default=0)
tags = ListField(StringField())
published = BooleanField(default=False)
# counter increment - the canonical use case
Post.objects(id=post.id).update(inc__views=1)
# set values by field name
Post.objects(id=post.id).update(set__title="New title")
# append to a list, idempotently or not
Post.objects(id=post.id).update(push__tags="hot")
Post.objects(id=post.id).update(addToSet__tags="hot") # no duplicates
# remove a list element and unset a field
Post.objects(id=post.id).update(pull__tags="cold", unset__draft=True)
The operator__field=value syntax maps directly to MongoDB update operators: inc, set, push, addToSet, pull, unset, mul, and max/min for bounds. The write is atomic on the server, no document is loaded, and no validation runs - which is exactly what a view counter wants.
Bulk Updates Across Many Documents
# one server-side operation for every match
n = Post.objects(published=False, created_at__lt=old_cutoff).update(
set__archived=True)
print("updated:", n) # the number of matched-and-modified documents
Queryset update() returns the count of documents modified, which is your audit number for bulk operations. Compare the loop alternative - fetch every match, change it, save() each - and the atomic form wins on latency, memory, and consistency (the whole cohort changes as one operation, no partial state).
Multi-Field and Conditional Updates
# several operators in one call, applied atomically together
Post.objects(slug="hello").update(
inc__views=1,
set__last_viewed=datetime.datetime.now(datetime.timezone.utc),
addToSet__tags="viewed",
)
# update only if a condition still holds (findAndModify-style guard)
Post.objects(id=post.id, published=False).update(set__published=True)
Every operator in a single update() call is one atomic server operation - the counter and the timestamp cannot partially apply. Adding extra filter kwargs (as in the last example) narrows the write to documents matching both the base queryset and the guard, which is how "publish only if not already published" stays safe under concurrency.
Choosing the Path
- Use
save()when the change needs the document's state, when validation must run, or when the instance is already in memory. - Use queryset
update()for counters, status flags, tag maintenance, and bulk cohorts - the write is cheaper, atomic, and race-safe for the field being updated. - Mixed changes: if a request both reads the document (to render a response) and bumps a counter, do the counter with an atomic
inc__and the rest withsave()- or, cleanest, one atomicupdate()plus a fresh read for the response.
Summary
Update with save() when you need the document and its validation, and with queryset update(inc__=..., set__=..., push__=..., unset__=...) when the change is a field-level write that should be atomic and cheap. Batch the same change across a cohort in one queryset call instead of a save-loop, use the returned count as the audit number, and remember that atomic updates skip validation - keep the invariants that matter at the field level so server-side writes cannot violate them.