Skip to content

Engine

Creating the engine

In the previous examples, we created the engine using default parameters:

  • MongoDB: running on localhost port 27017

  • Database name: test

It's possible to provide a custom AsyncIOMotorClient to the AIOEngine constructor. In the same way, the database name can be changed using the database keyword argument.

1
2
3
4
5
6
from motor.motor_asyncio import AsyncIOMotorClient

from odmantic import AIOEngine

client = AsyncIOMotorClient("mongodb://localhost:27017/")
engine = AIOEngine(motor_client=client, database="example_db")

For additional information about the MongoDB connection strings, see this section of the MongoDB documentation.

Usage with DNS SRV records

If you decide to use the DNS Seed List Connection Format (i.e mongodb+srv://...), you will need to install the dnspython package.

Create

There are two ways of persisting instances to the database (i.e creating new documents):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from odmantic import AIOEngine, Model


class Player(Model):
    name: str
    game: str


engine = AIOEngine()

leeroy = Player(name="Leeroy Jenkins", game="World of Warcraft")
await engine.save(leeroy)

players = [
    Player(name="Shroud", game="Counter-Strike"),
    Player(name="Serral", game="Starcraft"),
    Player(name="TLO", game="Starcraft"),
]
await engine.save_all(players)
Resulting documents in the player collection
{
  "_id": ObjectId("5f85f36d6dfecacc68428a46"),
  "game": "World of Warcraft",
  "name": "Leeroy Jenkins"
}
{
  "_id": ObjectId("5f85f36d6dfecacc68428a47"),
  "game": "Counter-Strike",
  "name": "Shroud"
}
{
  "_id": ObjectId("5f85f36d6dfecacc68428a49"),
  "game": "Starcraft",
  "name": "TLO"
}
{
  "_id": ObjectId("5f85f36d6dfecacc68428a48"),
  "game": "Starcraft",
  "name": "Serral"
}

Referenced instances

When calling AIOEngine.save or AIOEngine.save_all, the referenced models will be persisted as well.

Upsert behavior

The save and save_all methods behave as upsert operations (more details). Hence, you might overwrite documents if you save instances with an existing primary key already existing in the database.

Read

Examples database content

The next examples will consider that you have a player collection populated with the documents previously created.

Fetch a single instance

As with regular MongoDB driver, you can use the AIOEngine.find_one method to get at most one instance of a specific Model. This method will either return an instance matching the specified criteriums or None if no instances have been found.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from odmantic import AIOEngine, Model


class Player(Model):
    name: str
    game: str


engine = AIOEngine()

player = await engine.find_one(Player, Player.name == "Serral")
print(repr(player))
#> Player(id=ObjectId(...), name="Serral", game="Starcraft")

another_player = await engine.find_one(
    Player, Player.name == "Player_Not_Stored_In_Database"
)
print(another_player)
#> None

Missing values in documents

While parsing the MongoDB documents into Model instances, ODMantic will use the provided default values to populate the missing fields.

See this section for more details about document parsing.

Fetch using sort

We can use the sort parameter to fetch the Player instance with the first name in ascending order:

await engine.find_one(Player, sort=Player.name)
Find out more on sort in the dedicated section.

Fetch multiple instances

To get more than one instance from the database at once, you can use the AIOEngine.find method.

This method will return an AIOCursor object, that can be used in two different ways.

Usage as an async iterator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from odmantic import AIOEngine, Model


class Player(Model):
    name: str
    game: str


engine = AIOEngine()

async for player in engine.find(Player, Player.game == "Starcraft"):
    print(repr(player))

#> Player(id=ObjectId(...), name='TLO', game='Starcraft')
#> Player(id=ObjectId(...), name='Serral', game='Starcraft')

Ordering instances

The sort parameter allows to order the query in ascending or descending order on a single or multiple fields.

engine.find(Player, sort=(Player.name, Player.game.desc()))
Find out more on sort in the dedicated section.

Usage as an awaitable

Even if the async iterator usage should be preferred, in some cases it might be required to gather all the documents from the database before processing them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from odmantic import AIOEngine, Model


class Player(Model):
    name: str
    game: str


engine = AIOEngine()

players = await engine.find(Player, Player.game != "Starcraft")
print(players)
#> [
#>     Player(id=ObjectId(...), name="Leeroy Jenkins", game="World of Warcraft"),
#>     Player(id=ObjectId(...), name="Shroud", game="Counter-Strike"),
#> ]

Pagination

You can as well use the skip and limit keyword arguments when using AIOEngine.find, respectively to skip a specified number of instances and to limit the number of fetched instances.

Referenced instances

When calling AIOEngine.find or AIOEngine.find_one, the referenced models will be recursively resolved as well.

Passing the model class to find and find_one

When using the method to retrieve instances from the database, you have to specify the Model you want to query on as the first positional parameter. Internally, this enables ODMantic to properly type the results.

Count instances

You can count instances in the database by using the AIOEngine.count method. It's possible as well to use this method with filtering queries.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from odmantic import AIOEngine, Model


class Player(Model):
    name: str
    game: str


engine = AIOEngine()

player_count = await engine.count(Player)
print(player_count)
#> 4
cs_count = await engine.count(Player, Player.game == "Counter-Strike")
print(cs_count)
#> 1
valorant_count = await engine.count(Player, Player.game == "Valorant")
print(valorant_count)
#> 0

Combining multiple queries in read operations

While using find, find_one or count, you may pass as many queries as you want as positional arguments. Those will be implicitly combined as single and_ query.

Update

Updating an instance in the database can be done simply by modifying the instance locally and saving it again to the database.

The AIOEngine.save and AIOEngine.save_all methods are actually behaving as upsert operations. In other words, if the instance already exists it will be updated. Otherwise, the related document will be created in the database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from odmantic import AIOEngine, Model


class Player(Model):
    name: str
    game: str


engine = AIOEngine()
shroud = await engine.find_one(Player, Player.name == "Shroud")
print(shroud.game)
#> Counter-Strike
shroud.game = "Valorant"
await engine.save(shroud)
Resulting documents in the player collection
{
  "_id": ObjectId("5f85f36d6dfecacc68428a46"),
  "game": "World of Warcraft",
  "name": "Leeroy Jenkins"
}
{
  "_id": ObjectId("5f85f36d6dfecacc68428a47"),
  "game": "Valorant",
  "name": "Shroud"
}
{
  "_id": ObjectId("5f85f36d6dfecacc68428a49"),
  "game": "Starcraft",
  "name": "TLO"
}
{
  "_id": ObjectId("5f85f36d6dfecacc68428a48"),
  "game": "Starcraft",
  "name": "Serral"
}

Primary field update

Currently, changing the primary field value is disabled and a NotImplementedError exception will be raised if you try to do so. It is still possible to mutate the primary field if the field type is mutable but it might result in unexpected behaviors.

Workaround to modify the primary key

There is still a workaround to update the primary key of a document:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from bson import ObjectId

from odmantic import AIOEngine, Model


class Player(Model):
    name: str
    game: str


engine = AIOEngine()

shroud = await engine.find_one(Player, Player.name == "Shroud")
print(shroud.id)
#> 5f86074f6dfecacc68428a62
new_id = ObjectId("ffffffffffffffffffffffff")
# First delete the remote instance
await engine.delete(shroud)
# Then, copy the player object with a new primary key
new_shroud = Player(**{**shroud.dict(), "id": new_id})
# Finally create again the document
await engine.save(new_shroud)

Resulting document associated to the player

{
    "_id": ObjectId("ffffffffffffffffffffffff"),
    "game": "Valorant",
    "name": "Shroud"
}

Delete

You can delete instance by passing them to the AIOEngine.delete method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from odmantic import AIOEngine, Model


class Player(Model):
    name: str
    game: str


engine = AIOEngine()

players = await engine.find(Player)

for player in players:
    await engine.delete(player)

The collection is now empty 🧹.