Skip to content

Inheritance

Inheritance for multi-model use case

Beanie Documents support inheritance as any other Python classes. But there are additional features available if you mark the root model with the parameter is_root = True in the inner Settings class.

This behavior is similar to UnionDoc, but you don't need an additional entity. Parent Document act like a "controller", that handles proper storing and fetches different Document types. Also, parent Document can have some shared attributes which are propagated to all children. All classes in the inheritance chain can be used as Link in foreign Documents.

Depending on the business logic, parent Document can be like an "abstract" class that is not used to store objects of its type (like in the example below), as well as can be a full-fledged entity, like its children.

Defining models

To set the root model you have to set is_root = True in the inner Settings class. All the inherited documents (on any level) will be stored in the same collection.

from typing import Optional, List
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel
from beanie import Document, Link, init_beanie


class Vehicle(Document):
    """Inheritance scheme bellow"""
    #               Vehicle
    #              /   |   \
    #             /    |    \
    #        Bicycle  Bike  Car
    #                         \
    #                          \
    #                          Bus
    # shared attribute for all children
    color: str

    class Settings:
        is_root = True


class Fuelled(BaseModel):
    """Just a mixin"""
    fuel: Optional[str]


class Bicycle(Vehicle):
    """Derived from Vehicle, will use its collection"""
    frame: int
    wheels: int


class Bike(Vehicle, Fuelled):
    ...


class Car(Vehicle, Fuelled):
    body: str


class Bus(Car, Fuelled):
    """Inheritance chain is Vehicle -> Car -> Bus, it is also stored in Vehicle collection"""
    seats: int


class Owner(Document):
    vehicles: Optional[List[Link[Vehicle]]]

Inserts

Inserts work the same way as usual

client = AsyncIOMotorClient()
await init_beanie(client.test_db, document_models=[Vehicle, Bicycle, Bike, Car, Bus])

bike_1 = await Bike(color='black', fuel='gasoline').insert()

car_1 = await Car(color='grey', body='sedan', fuel='gasoline').insert()
car_2 = await Car(color='white', body='crossover', fuel='diesel').insert()

bus_1 = await Bus(color='white', seats=80, body='bus', fuel='diesel').insert()
bus_2 = await Bus(color='yellow', seats=26, body='minibus', fuel='diesel').insert()

owner = await Owner(name='John', vehicles=[car_1, car_2, bus_1]).insert()

Find operations

With parameter with_children = True the find query results will contain all the children classes' objects.

# this query returns vehicles of all types that have white color, because `with_children` is True
white_vehicles = await Vehicle.find(Vehicle.color == 'white', with_children=True).to_list()
# [
#    Bicycle(..., color='white', frame=54, wheels=29),
#    Car(fuel='diesel', ..., color='white', body='crossover'),
#    Bus(fuel='diesel', ..., color='white', body='bus', seats=80)
# ]

If the search is based on a child, the query returns this child type and all sub-children (with parameter with_children=True)

cars_and_buses = await Car.find(Car.fuel == 'diesel', with_children=True).to_list()
# [
#     Car(fuel='diesel', ..., color='white', body='crossover'),
#     Bus(fuel='diesel', ..., color='white', body='bus', seats=80),
#     Bus(fuel='diesel', ..., color='yellow', body='minibus', seats=26)
# ]

If you need to return objects of the specific class only, you can use this class for finding:

# however it is possible to limit by Vehicle type
cars_only = await Car.find().to_list()
# [
#     Car(fuel='gasoline', ..., color='grey', body='sedan'),
#     Car(fuel='diesel', ..., color='white', body='crossover')
# ]

To get a single Document it is not necessary to know the type. You can query using the parent class

await Vehicle.get(bus_2.id, with_children=True)
# returns Bus instance:
# Bus(fuel='diesel', ..., color='yellow', body='minibus', seats=26)

Relations

Linked documents will be resolved into the respective classes

owner = await Owner.get(owner.id, fetch_links=True)

print(owner.vehicles)
# [
#    Car(fuel='diesel', ..., color='white', body='crossover'),
#    Bus(fuel='diesel', ..., color='white', body='bus', seats=80),
#    Car(fuel='gasoline', ..., color='grey', body='sedan')
# ]

The same result will be if the owner gets objects without fetching the links, and they will be fetched manually later

Other

All other operations work the same way as for simple Documents

await Bike.find().update({"$set": {Bike.color: 'yellow'}})
await Car.find_one(Car.body == 'sedan')