Skip to content

# Raw query usage

As ODMantic doesn't completely wrap the MongoDB API, some helpers are provided to be enhance the usability while building raw queries and interacting with raw documents.

## Raw query helpers

### Collection name

You can get the collection name associated to a model by using the unary `+` operator on the model class.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10

    from odmantic import Model

    class User(Model):
        name: str

    collection_name = +User
    print(collection_name)
    #> user

### Motor collection

The AIOEngine object can provide you directly the motor collection (AsyncIOMotorCollection) linked to the motor client used by the engine. To achieve this, you can use the AIOEngine.get_collection method.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    from odmantic import AIOEngine, Model

    class User(Model):
        name: str

    engine = AIOEngine()
    motor_collection = engine.get_collection(User)
    print(motor_collection)
    #> AsyncIOMotorCollection(
    #>     Collection(
    #>         Database(
    #>             MongoClient(
    #>                 host=["localhost:27017"],
    #>                 document_class=dict,
    #>                 tz_aware=False,
    #>                 connect=False,
    #>                 driver=DriverInfo(name="Motor", version="2.2.0", platform="asyncio"),
    #>             ),
    #>             "test",
    #>         ),
    #>         "user",
    #>     )
    #> )

### PyMongo collection

The SyncEngine object can provide you directly the PyMongo collection (pymongo.collection.Collection) linked to the PyMongo client used by the engine. To achieve this, you can use the SyncEngine.get_collection method.

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

    from odmantic import SyncEngine, Model

    class User(Model):
        name: str

    engine = SyncEngine()
    collection = engine.get_collection(User)
    print(collection)
    #> Collection(
    #>     Database(
    #>         MongoClient(
    #>             host=["localhost:27017"],
    #>             document_class=dict,
    #>             tz_aware=False,
    #>             connect=True,
    #>         ),
    #>         "test",
    #>     ),
    #>     "user",
    #> )

### Key name of a field

Since some field might have some customized key names, you can get the key name associated to a field by using the unary `+` operator on the model class. As well, to ease the use of aggregation pipelines where you might need to reference your field (`$field`), you can double the operator (i.e use `++`) to get the field reference name.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12

    from odmantic import Field, Model

    class User(Model):
        name: str = Field(key_name="username")

    print(+User.name)
    #> username

    print(++User.name)
    #> $username

## Using raw MongoDB filters

Any QueryExpression can be replaced by its raw filter equivalent.

For example, with a Tree model:

    1
    2
    3
    4
    5
    6
    7
    8
    9

    from odmantic import AIOEngine, Model

    class Tree(Model):
        name: str
        average_size: float

    engine = AIOEngine()

All the following find queries would give exactly the same results:

    engine.find(Tree, Tree.average_size > 2)
    engine.find(Tree, {+Tree.average_size: {"$gt": 2}})
    engine.find(Tree, {"average_size": {"$gt": 2}})

## Raw MongoDB documents

### Parsing documents

You can parse MongoDB document to instances using the model_validate_doc method.

Tip

If the provided documents contain extra fields, ODMantic will ignore them. This can be especially useful in aggregation pipelines.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14

    from bson import ObjectId

    from odmantic import Field, Model

    class User(Model):
        name: str = Field(key_name="username")

    document = {"username": "John", "_id": ObjectId("5f8352a87a733b8b18b0cb27")}

    user = User.model_validate_doc(document)
    print(repr(user))
    #> User(id=ObjectId('5f8352a87a733b8b18b0cb27'), name='John')

### Dumping documents

You can generate a document from instances using the model_dump_doc method.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10

    from odmantic import Field, Model

    class User(Model):
        name: str = Field(key_name="username")

    user = User(name="John")
    print(user.model_dump_doc())
    #> {'username': 'John', '_id': ObjectId('5f8352a87a733b8b18b0cb27')}

### Advanced parsing behavior

#### Default values

While parsing documents, ODMantic will use the default values provided in the Models to populate the missing fields from the documents:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    from bson import ObjectId

    from odmantic import Model

    class Player(Model):
        name: str
        level: int = 1

    document = {"name": "Leeroy", "_id": ObjectId("5f8352a87a733b8b18b0cb27")}

    user = Player.model_validate_doc(document)
    print(repr(user))
    #> Player(
    #>     id=ObjectId("5f8352a87a733b8b18b0cb27"),
    #>     name="Leeroy",
    #>     level=1,
    #> )

#### Default factories

For the field with default factories provided through the Field descriptor though, by default they wont be populated.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    from datetime import datetime

    from bson import ObjectId

    from odmantic import Model
    from odmantic.exceptions import DocumentParsingError
    from odmantic.field import Field

    class User(Model):
        name: str
        created_at: datetime = Field(default_factory=datetime.utcnow)

    document = {"name": "Leeroy", "_id": ObjectId("5f8352a87a733b8b18b0cb27")}

    try:
        User.model_validate_doc(document)
    except DocumentParsingError as e:
        print(e)
        #> 1 validation error for User
        #> created_at
        #>   key not found in document (type=value_error.keynotfoundindocument; key_name='created_at')
        #> (User instance details: id=ObjectId('5f8352a87a733b8b18b0cb27'))

In the previous example, using the default factories could create data inconsistencies and in this case, it would probably be more suitable to perform a manual migration to provide the correct values.

Still, the `parse_doc_with_default_factories` Config option can be used to allow the use of the default factories while parsing documents:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    from datetime import datetime

    from bson import ObjectId

    from odmantic import Model
    from odmantic.exceptions import DocumentParsingError
    from odmantic.field import Field

    class User(Model):
        name: str
        updated_at: datetime = Field(default_factory=datetime.utcnow)

        model_config = {"parse_doc_with_default_factories": True}

    document = {"name": "Leeroy", "_id": ObjectId("5f8352a87a733b8b18b0cb27")}

    user = User.model_validate_doc(document)
    print(repr(user))
    #> User(
    #>     id=ObjectId("5f8352a87a733b8b18b0cb27"),
    #>     name="Leeroy",
    #>     updated_at=datetime.datetime(2020, 11, 8, 23, 28, 19, 980000),
    #> )

## Aggregation example

In the following example, we will demonstrate the use of the previous helpers to build an aggregation pipeline. We will first consider a `Rectangle` model with two float fields (`height` and `length`). We will then fetch the rectangles with an area that is less than 10. To finish, we will reconstruct `Rectangle` instances from this query.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49

    from odmantic import AIOEngine, Model

    class Rectangle(Model):
        length: float
        width: float

    rectangles = [
        Rectangle(length=0.1, width=1),
        Rectangle(length=3.5, width=1),
        Rectangle(length=2.87, width=5.19),
        Rectangle(length=1, width=10),
        Rectangle(length=0.1, width=100),
    ]

    engine = AIOEngine()
    await engine.save_all(rectangles)

    collection = engine.get_collection(Rectangle)
    pipeline = []
    # Add an area field
    pipeline.append(
        {
            "$addFields": {
                "area": {
                    "$multiply": [++Rectangle.length, ++Rectangle.width]
                }  # Compute the area remotely
            }
        }
    )
    # Filter only rectanges with an area lower than 10
    pipeline.append({"$match": {"area": {"$lt": 10}}})
    # Project to keep only the defined fields (this step is optional)
    pipeline.append(
        {
            "$project": {
                +Rectangle.length: True,
                +Rectangle.width: True,
            }  # Specifying "area": False is unnecessary here
        }
    )
    documents = await collection.aggregate(pipeline).to_list(length=None)
    small_rectangles = [Rectangle.model_validate_doc(doc) for doc in documents]
    print(small_rectangles)
    #> [
    #>     Rectangle(id=ObjectId("..."), length=0.1, width=1.0),
    #>     Rectangle(id=ObjectId("..."), length=3.5, width=1.0),
    #> ]