4. CRUD Operations¶
4.1 Creating Documents¶
Documents are created through the Document API’s save() method, which supports multiple modes:
Default mode: Auto-detects insert/update based on identity presence. New documents must have
Noneidentity before creation.Explicit insert:
save(mode="insert")forces creation (fails on duplicate)Upsert:
save(mode="upsert")combines insert/update for known identities
Linked documents must be saved separately before being referenced - they should be provided as complete model instances during document creation. The save operation returns documents with their generated identities while leaving any linked documents unchanged (no lookup pipeline activation for references).
Documents can alternatively be created through update_document() with upsert=True, which performs direct MongoDB
upsert operations without going through the full document lifecycle hooks.
Example of documents creation with save():
class Department(SerialIDDocument):
name: str
class User(SerialIDDocument):
department: Department
name: str
async def main():
department = await Department(name="IT").save()
await User(name="Vasya Pupkin", department=department).save()
Example of documents creation with update_document():
class SerialIDCounter(Document[str]):
name: Annotated[str, IdentityField()]
count: int
async def main():
await SerialIDCounter.update_document(
"foo",
Inc({F(SerialIDCounter.count): 1}),
upsert=True,
)
4.2 Reading and Counting Documents¶
Butty provides several query methods with automatic pipeline processing and nested query support:
get(): Fetch by exact identity match (raisesDocumentNotFoundif missing), identity values are type-checked against the document’sID_Tparameterfind_one(): Retrieve first matching document (raisesDocumentNotFoundif none match)find_one_or_none(): Returns first match orNoneif none foundfind(): Returns paginated and sorted list of matching documents (supportsskip,limit, andsortparameters)find_iter(): Async generator for large result sets (supports same pagination/sorting asfind())count_documents(): Returns matching document countfind_and_count(): Combined query with total count (optimized with$facetaggregation)
All read operations:
Activate the full lookup pipeline including relationship resolution
Support nested querying across document relationships
The find_and_count() method is particularly optimized, executing both the query and count in a single database request
using the $facet aggregation operator.
Example of document querying:
async def main():
user = await User.get(1)
users = await User.find(F(User.department.name) == "IT")
4.3 Updating Documents¶
Updates can be performed through:
Standard
save()cycle (read-modify-save)Direct
update()by identityAtomic
update_document()by identity and update query (Set()andInc()are supported for now), not supported for versioned documents
The save() method’s update mode provides version checking when configured.
4.4 Deleting Documents¶
Document deletion requires a full document instance to properly execute relationship handling. The system supports three
deletion modes through LinkField configuration: “nothing” (default), “cascade” (deletes referencing documents), and
“propagate” (deletes referenced documents). The “propagate” mode works with both single references and collections
(array/dict) of referenced documents. The current implementation processes these operations sequentially rather than
atomically.
The delete() operation triggers before_delete hooks prior to removal, allowing for custom cleanup logic, e.g.
removing files in storage associated with the documents. These hooks execute before any relationship processing begins.
Unlike update operations, document deletion does not perform version validation.
Deleted documents are returned with their identity field cleared.
Example of document deletion:
class Department(SerialIDDocument):
name: str
class User(SerialIDDocument):
department: Annotated[Department, LinkField(on_delete="cascade")]
name: str
async def main():
department = await Department(name="IT").save()
await User(name="Vasya Pupkin", department=department).save()
department = await Department.find_one(F(Department.name) == "IT")
await department.delete() # also deletes associated users