Skillversitas

Saving and Querying Documents

The CRUD core of MongoEngine is deliberately small: save() writes, Document.objects starts a query, and the queryset methods (filter, first, get, count) do the rest. The one concept that makes it click is laziness - a queryset is a description of a query, not a list of documents, and the database call happens only when you ask for results.

Saving

from mongoengine import Document, StringField

class Note(Document):
    text = StringField(required=True)

# create
note = Note(text="first note")
note.save()                          # insert (new _id)

# update the same document
note.text = "edited"
note.save()                          # update by _id

# forced insert that fails on duplicate _id
Note(text="dup", id=note.id).save(insert=True)   # raises NotUniqueError

Querying with the QuerySet

# the query is built lazily; nothing hits the server yet
qs = Note.objects.filter(text__startswith="first")

note = qs.first()                    # executes: returns the first match or None
all_notes = list(qs)                 # executes: all matches
count = qs.count()                   # executes: a count, no documents

# exactly one expected
note = Note.objects.get(id=note.id)  # NotUniqueError if >1, DoesNotExist if 0

# field selection - fetch less over the wire
qs = Note.objects.only("text")       # projection: only the declared field

The only()/exclude() projections are the cheap performance lever: on a collection with heavy fields, fetching only("text", "updated_at") for a list view cuts the transfer dramatically. The _id is always returned unless you pass only("text") without it in a context that allows it.

Ordering, Limiting, Skipping

recent = Note.objects.order_by("-created_at")[:25]   # newest first, 25 rows
page2  = Note.objects.order_by("-created_at").skip(25)[:25]

# meta ordering on the class is the default sort
class Note(Document):
    meta = {"ordering": ["-created_at"]}

Slicing a queryset ([:25]) becomes a MongoDB limit; skip() becomes skip. Slicing is the idiomatic limit - qs[:10] - and it composes with order_by, which is how pagination is built. For large offsets, keyset pagination (filter on the last seen value, order by it, limit) beats skip because skip cost grows with the offset.

When the Query Actually Runs

  • Iteration: for doc in qs (MongoDB cursor, fetched in batches).
  • Indexing/slicing: qs[0], qs[:10], len(qs).
  • Terminal methods: first(), get(), count(), average(), sum(), delete(), update(...).
  • A queryset can be reused after execution, but re-execution is a fresh database call - it is not a cached list.

Dropping to Raw When Needed

# the queryset knows its own collection and filter
collection = Note.objects(text__startswith="a").as_pymongo()
cursor = collection.find()
print(next(cursor))     # a plain dict, no ODM wrapper

as_pymongo() returns the underlying pymongo collection with the filter applied - the escape hatch for operations the ODM does not wrap (distinct, some find options, bulk raw writes). It is also how you verify that the ODM is generating the query you think it is.

Summary

Write with save() (insert for new _id, update for existing), read through Document.objects - build with filter, execute with first/get/count/iteration, and shape the transfer with only(), order_by, and slicing. Remember the queryset is lazy and re-runs when re-used, use get when exactly one result is the contract, and keep as_pymongo() available for the operations that need raw driver access.