Skip to content

Model Inheritance

We can implement model inheritance at the collection level.

Scenario

Let's imagine we want to implement a course system.

The course can have multiple types of content like text, video, image, file, etc. But all share some common fields like course_id, created_at, etc. And content should be listed.

We can use this functionality relationally, but it will be expensive to query because we need multiple lookups.

In this scenario, we can implement Model Inheritance provided by MongoDB-ODM. (Note: Currently, only one level of inheritance is supported.)

Define Model

In this section, we will use multiple models to implement model inheritance.

Overview

First look at all models in a single block for better understanding.

# Code omitted above

class Course(Document):
    title: str


class Content(Document):
    course_id: ODMObjectId
    title: str = Field(max_length=255)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = True
        indexes = [
            IndexModel([("course_id", ASCENDING)]),
        ]


class Text(Content):
    text: str

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False


class Video(Content):
    video_path: str = Field(max_length=512)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False

# Code omitted below
Full file preview
import os

from mongodb_odm import (
    ASCENDING,
    Document,
    Field,
    IndexModel,
    ODMObjectId,
    apply_indexes,
    connect,
)


class Course(Document):
    title: str


class Content(Document):
    course_id: ODMObjectId
    title: str = Field(max_length=255)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = True
        indexes = [
            IndexModel([("course_id", ASCENDING)]),
        ]


class Text(Content):
    text: str

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False


class Video(Content):
    video_path: str = Field(max_length=512)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False


def configuration():
    connect(os.environ.get("MONGO_URL", "mongodb://localhost:27017/testdb"))
    apply_indexes()


def create_data():
    course = Course(title="MongoDB-ODM Tutorial").create()

    Text(course_id=course.id, title="Introduction", text="Introduction Text").create()
    Video(
        course_id=course.id,
        title="Environment Setup",
        video_path="/media/video_path.mp4",
    ).create()


def retrieve_content():
    for content in Content.find():
        print(content)
    print()


def retrieve_text():
    for text in Text.find():
        print(text)
    print()


def retrieve_video():
    for video in Video.find():
        print(video)
    print()


def main():
    configuration()

    create_data()

    retrieve_content()
    retrieve_text()
    retrieve_video()


if __name__ == "__main__":
    main()

We want to keep everything simple. First, we define the course, where courses only have one field: title.

Content

We define the Content model. We want to inherit the content model in the Text and Video models.

In the ODMConfig class for the Content model, we define allow_inheritance = True. To make a model inheritable, we need to set this field to True.

class Content(Document):
    course_id: ODMObjectId
    title: str = Field(max_length=255)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = True
        indexes = [
            IndexModel([("course_id", ASCENDING)]),
        ]

Text

After defining the Content model, we will define the Text model that inherits from the Content model to have all the functionality of Content.

Set allow_inheritance = False for the Text model. Otherwise, MongoDB-ODM will throw an error.

class Text(Content):
    text: str

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False

Video

Like the Text functionality and declaration structure, the Video will be the same.

class Video(Content):
    video_path: str = Field(max_length=512)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False

Insert Data

Let's insert some data in the Course and Content collection.

First, we create a course document.

Then we create two Content documents: one Text content and one video content.

def create_data():
    course = Course(title="MongoDB-ODM Tutorial").create()

    Text(course_id=course.id, title="Introduction", text="Introduction Text").create()
    Video(
        course_id=course.id,
        title="Environment Setup",
        video_path="/media/video_path.mp4",
    ).create()
Full file preview
import os

from mongodb_odm import (
    ASCENDING,
    Document,
    Field,
    IndexModel,
    ODMObjectId,
    apply_indexes,
    connect,
)


class Course(Document):
    title: str


class Content(Document):
    course_id: ODMObjectId
    title: str = Field(max_length=255)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = True
        indexes = [
            IndexModel([("course_id", ASCENDING)]),
        ]


class Text(Content):
    text: str

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False


class Video(Content):
    video_path: str = Field(max_length=512)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False


def configuration():
    connect(os.environ.get("MONGO_URL", "mongodb://localhost:27017/testdb"))
    apply_indexes()


def create_data():
    course = Course(title="MongoDB-ODM Tutorial").create()

    Text(course_id=course.id, title="Introduction", text="Introduction Text").create()
    Video(
        course_id=course.id,
        title="Environment Setup",
        video_path="/media/video_path.mp4",
    ).create()


def retrieve_content():
    for content in Content.find():
        print(content)
    print()


def retrieve_text():
    for text in Text.find():
        print(text)
    print()


def retrieve_video():
    for video in Video.find():
        print(video)
    print()


def main():
    configuration()

    create_data()

    retrieve_content()
    retrieve_text()
    retrieve_video()


if __name__ == "__main__":
    main()

Impact in Database

For model inheritance, we do not create a separate collection for each model. Instead, we create a single collection for all models, and the collection name will be according to the parent class. We add an extra field _cls with each document to distinguish between the different models.

Tip

By default, we define the _cls field as an index field. We can remove the index for this field by defining index_inheritance_field = False in the parent ODMConfig.

class Content(Document):
    ...
    class ODMConfig(Document.ODMConfig):
        index_inheritance_field = False

Retrieve Collection

We can use all our retrieval methods to retrieve data for the inherited model. We will add an extra filter key {"_cls": '<Model Name>'} to filter out the targeted children, or no extra filter for parents.

Filter Content

To get all content, we can filter data with the parent class Content.

def retrieve_content():
    for content in Content.find():
        print(content)
    print()
Full file preview
import os

from mongodb_odm import (
    ASCENDING,
    Document,
    Field,
    IndexModel,
    ODMObjectId,
    apply_indexes,
    connect,
)


class Course(Document):
    title: str


class Content(Document):
    course_id: ODMObjectId
    title: str = Field(max_length=255)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = True
        indexes = [
            IndexModel([("course_id", ASCENDING)]),
        ]


class Text(Content):
    text: str

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False


class Video(Content):
    video_path: str = Field(max_length=512)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False


def configuration():
    connect(os.environ.get("MONGO_URL", "mongodb://localhost:27017/testdb"))
    apply_indexes()


def create_data():
    course = Course(title="MongoDB-ODM Tutorial").create()

    Text(course_id=course.id, title="Introduction", text="Introduction Text").create()
    Video(
        course_id=course.id,
        title="Environment Setup",
        video_path="/media/video_path.mp4",
    ).create()


def retrieve_content():
    for content in Content.find():
        print(content)
    print()


def retrieve_text():
    for text in Text.find():
        print(text)
    print()


def retrieve_video():
    for video in Video.find():
        print(video)
    print()


def main():
    configuration()

    create_data()

    retrieve_content()
    retrieve_text()
    retrieve_video()


if __name__ == "__main__":
    main()

After running this function, the printed object should look like this.

Text(id=ObjectId('id'), course_id=ObjectId('id'), title='Introduction', text='Introduction Text', _id=ObjectId('id'))
Video(id=ObjectId('id'), course_id=ObjectId('id'), title='Environment Setup', video_path='/media/video_path.mp4', _id=ObjectId('id'))

Retrieve Text and Video

We can retrieve data using the Text and Video models.

def retrieve_text():
    for text in Text.find():
        print(text)
    print()


def retrieve_video():
    for video in Video.find():
        print(video)
    print()
Full file preview
import os

from mongodb_odm import (
    ASCENDING,
    Document,
    Field,
    IndexModel,
    ODMObjectId,
    apply_indexes,
    connect,
)


class Course(Document):
    title: str


class Content(Document):
    course_id: ODMObjectId
    title: str = Field(max_length=255)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = True
        indexes = [
            IndexModel([("course_id", ASCENDING)]),
        ]


class Text(Content):
    text: str

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False


class Video(Content):
    video_path: str = Field(max_length=512)

    class ODMConfig(Document.ODMConfig):
        allow_inheritance = False


def configuration():
    connect(os.environ.get("MONGO_URL", "mongodb://localhost:27017/testdb"))
    apply_indexes()


def create_data():
    course = Course(title="MongoDB-ODM Tutorial").create()

    Text(course_id=course.id, title="Introduction", text="Introduction Text").create()
    Video(
        course_id=course.id,
        title="Environment Setup",
        video_path="/media/video_path.mp4",
    ).create()


def retrieve_content():
    for content in Content.find():
        print(content)
    print()


def retrieve_text():
    for text in Text.find():
        print(text)
    print()


def retrieve_video():
    for video in Video.find():
        print(video)
    print()


def main():
    configuration()

    create_data()

    retrieve_content()
    retrieve_text()
    retrieve_video()


if __name__ == "__main__":
    main()

After executing the retrieve_text and retrieve_video functions the output should look like this.

Text(id=ObjectId('id'), course_id=ObjectId('id'), title='Introduction', text='Introduction Text', _id=ObjectId('id'))

Video(id=ObjectId('id'), course_id=ObjectId('id'), title='Environment Setup', video_path='/media/video_path.mp4', _id=ObjectId('id'))

find_one and get with inheritance

Like the find method, we can retrieve the child object from the parent using the find_one and get methods. If the retrieved object appears to be one of the children, then the object will have all of the properties of that child.