Announcing Beanie 1.0 - MongoDB ODM with Query Builder🚀🎉

romanright

Roman Right

Posted on May 11, 2021

Announcing Beanie 1.0 - MongoDB ODM with Query Builder🚀🎉

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),
            ],
        ]
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

Find queries

Now you can use Python native comparison operators with the document class fields:

Product.find(Product.category.name == "Chocolate")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

To retrieve the list, I use to_list() method.

products = await Product.find(
    Product.category.name == "Chocolate"
).to_list()
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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}))
Enter fullscreen mode Exit fullscreen mode

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}})

Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

To update all the documents without the find query you can skip the find step:

await Product.inc({Product.price: 2})
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
romanright
Roman Right

Posted on May 11, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related