Skip to content

Defining documents

Defining documents

As you probably already know, MongoDB databases have collections instead of tables. And each collection has documents instead of rows.

You can define your documents by inheriting from mongox.Model.

import asyncio

import mongox

client = mongox.Client("mongodb://localhost:27017")
db = client.get_database("test_db")


class Movie(mongox.Model, db=db):
    name: str
    year: int

First we create a mongox.Client instance with the MongoDB URI. Then we get a database from client by calling .get_database().

If you want to have more control over the client event loop, you can specify a callable to get event loop at runtime:

client = mongox.Client(
    "mongodb://localhost:27017", get_event_loop=asyncio.get_running_loop
)

By default, mongox.Client will use asyncio.get_running_loop.

Model attributes are defined the same way as Pydantic. The Movie class is both a mongox Model and also a pydantic BaseModel.

Now we have a Movie collection with attributes name and year.

Field validation

You can add field-level validations by using mongox.Field. This is actually just a short-cut to the Pydantic Field and accepts the same arguments.

Let's say we want to limit the year attribute of Movie to be more strict:

import mongox


class Movie(mongox.Model):
    name: str
    year: int = mongox.Field(gt=1800)

Now when creating a Movie instance, the year will be validated differently.

This will be ok:

await Movie(name="Forrest Gump", year=1994).insert()

But this will throw a Pydantic ValidationError:

await Movie(name="Golden Oldie", year=1790).insert()

# E   pydantic.error_wrappers.ValidationError: 1 validation error for Movie
# E   year
# E     ensure this value is greater than 1800 (type=value_error.number.not_gt; limit_value=1800)

Some of the most common Field arguments include:

For numeric types like int, float and Decimal:

  • gt Rquires the field to be greater than
  • ge Rquires the field to be greater than or equal to
  • le Rquires the field to be less than or equal to
  • lt Rquires the field to be less than
  • multiple_of Requires the field to be a multiple of

For strings:

  • min_length Requires the field to have a minimum length
  • max_length Requires the field to have a maximum length
  • regex Requires the field to match a regular expression

For a full list of Field arguments you can refer to the Pydantic docs here.

Defining indexes

Mongox models accept indexes as a list of mongox.Index instances:

import mongox


indexes = [
    mongox.Index("name", unique=True),
    mongox.Index(keys=[("year", mongox.Order.DESCENDING), ("genre", mongox.IndexType.HASHED)]),
]


class Movie(mongox.Model, db=db, indexes=indexes):
    name: str
    genre: str
    year: int 

For creating Index objects we have two options, for simple cases we can do:

Index(key_name, **kwargs)

But to have more control over index definition we can do:

Index(keys=[(key_name, index_order)], **kwargs)

And then, you can then create the collection indexes with:

await Movie.create_indexes()

Or to drop the indexes:

await Movie.drop_indexes()

Note that this will only drop indexes defined in Movie model and won't affect the ones manually created. To drop all indexes, even those not defined here you can pass force=True:

await Movie.drop_indexes(force=True)

And finally if you need to drop a single index by name:

await Movie.drop_index("year_genre)

Index accepts the following arguments:

  • key For single key (simple indexes).

  • keys List of keys and their types.

  • name Can be specified or automatically generated from keys.

  • background If the index creation should happen in the background.

  • unique If the index should be a unique index.

For example:

Index(keys=[("year", Order.DESCENDING)], name="year_index", background=True)

To specify order of index you can use mongox.Order:

from mongox import Order

Index(keys=[("year", Order.ASCENDING)], name="year_index", background=True)

Order class has only two attributes ASCENDING and DESCENDING.

And to specify custom index type, you can pass mongox.IndexType instead of index order:

from mongox import IndexType

Index(keys=[("year", IndexType.HASHED)], name="year_index", background=True)

IndexType has the supported PyMongo index types:

  • GEO2D

  • GEOSPHERE

  • HASHED

  • TEXT

For a full list of allowed arguments you can refer to the PyMongo docs here.

Embedded Models

Embedded Models are models to be embedded in mongox.Model. The difference is that EmbeddedModels won't be inserted separately and won't get their seaparate _id.

To define Embedded Models you should inherit from mongox.EmbeddedModel and define the fields the same as Pydantic.

import mongox


class Genre(mongox.EmbeddedModel):
    title: str


class Movie(mongox.Model):
    name: str
    genre: Genre

You can use EmbeddedModel to define multi-level nested objects in MongoDB.