Roman Right
Posted on May 11, 2021
WARNING!!
This article is outdated.
Please, follow the current documentation of Beanie to use actual features and patterns.
Link to the doc - https://roman-right.github.io/beanie/
I'm happy to introduce to you Beanie 1.0.0 - Python ODM (Object Document Mapper) for MongoDB!
Two months ago I published the very first Beanie release. You can find the article about it by the link. I demonstrated there, how simple it is, to make a CRUD service with FastAPI and Beanie.
Many features were added since then. Today I want to show the most interesting things, which came with this major version update.
For this demo, I will use the Product
document model.
from beanie import Document
from pydantic import BaseModel
class Category(BaseModel):
name: str
description: str
class Product(Document):
name: str
description: Optional[str] = None
price: Indexed(float, pymongo.DESCENDING)
category: Category
num: int
class Collection:
name = "products"
indexes = [
[
("name", pymongo.TEXT),
("description", pymongo.TEXT),
],
]
Beanie Document is an abstraction over Pydantic BaseModel. It helps to make documents flexible and structured at the same time.
For this demo I set up indexes here in two ways:
- Simple with the
Indexed
field - More complex with the
Collection
inner class
More information about document setup could be found by link
Create documents
Beanie provides and single document creation pattern and batch inserts:
chocolate = Category(name="Chocolate")
# One
caramel_and_nougat = Product(name="Caramel and Nougat ",
num=20,
price=3.05,
category=chocolate)
await caramel_and_nougat.create()
# Many
peanut_bar = Product(name="Peanut Bar",
num=4,
price=4.44,
category=chocolate)
truffle = Product(name="Chocolate Truffle",
num=40,
price=2.50,
category=chocolate)
await Product.insert_many([peanut_bar, truffle])
Find queries
Now you can use Python native comparison operators with the document class fields:
Product.find(Product.category.name == "Chocolate")
The find()
method will return the FindMany
query, which uses an async generator pattern, and data can be available via async for
loop.
async for item in Product.find(
Product.category.name == "Chocolate"
):
print(item)
To retrieve the list, I use to_list()
method.
products = await Product.find(
Product.category.name == "Chocolate"
).to_list()
FindMany
queries provide also sort, skip, limit, and project methods:
class ProductShortView(BaseModel):
name: str
price: float
products = await Product.find(
Product.category.name == "Chocolate",
Product.price < 3.5
).sort(-Product.price).limit(10).project(ProductShortView)
Python comparison operators don't cover all the cases. Beanie provides a list of find operators, which could be used instead:
from beanie.operators import Text
products = await Product.find(Text("Chocolate")).to_list()
The whole list of the find operators can be found here
You can use native PyMongo syntax for fine-tuning here too:
products = await Product.find({"price": {"gte": 2}}).to_list()
If you need to find a single document, you can use the find_one
method instead.
product = await Product.find_one(Product.name == "Peanut Bar")
The detailed tutorial about finding the documents can be found by link
Update
update()
method allows updating documents using search criteria of the FindMany
and FindOne
queries.
You can do it, using update operators:
from beanie.operators import Inc, Set
# Many
await Product.find(
Product.name == "Peanut Bar"
).update(Set({Product.price: 5}))
# One
await Product.find_one(
Product.name == "Chocolate Truffle"
).update(Set({Product.price: 3}))
# or
product = await Product.find_one(Product.name == "Peanut Bar")
await product.update(Inc({Product.price: -1}))
The list of the update operators can be found by link
Native PyMongo syntax is also supported for this
await Product.find(
Product.num <= 5
).update({"$set": {Product.price: 1}})
There is a list of preset update operations, which could be used as methods. Increment, for example:
await Product.find(
Product.category.name == "Chocolate"
).inc({Product.price: 2})
To update all the documents without the find query you can skip the find
step:
await Product.inc({Product.price: 2})
Aggregations
Aggregations, as updates, could be used over the whole collection or, usingFindMany
searching criteria.
class TotalCountView(BaseModel):
category: str = Field(None, alias="_id")
total: int
# Over collection
total_count = await Product.aggregate(
[{"$group": {"_id": "$category", "total": {"$sum": "$num"}}}],
projection_model=TotalCountView
).to_list()
# Subset
total_count = await Product.find(Product.price < 10).aggregate(
[{"$group": {"_id": "$category", "total": {"$sum": "$num"}}}],
projection_model=TotalCountView
).to_list()
As for the update operations, there is a list of preset methods for the popular aggregations. For example, average:
avg_choco_price = await Product.find(
Product.category.name == "Chocolate"
).avg(Product.price)
Here you can find the doc with all the preset methods.
Delete
Delete operations support the same patterns:
# Many
await Product.find(
Product.category.name == "Chocolate").delete()
# One
product = await Product.find_one(Product.name == "Peanut Bar")
await product.delete()
# Without fetching
await Product.find_one(
Product.name == "Chocolate Truffle"
).delete()
Conclusion
In my first article, I said, that Beanie is a micro ODM. I'm removing the prefix micro
now. Beanie is a rich Python ODM for MongoDB with a lot of features, like query builder, projections, and migrations. It helps me to build services and applications a lot. I hope, it will help many other developers too.
There is a list of interesting things, that I plan to add there. You are always welcome to participate in the development :-) Thank you very much for your time!
Links
Posted on May 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.