Skip to content

Many to Many Relationship Intro

In a traditional SQL database, we use a 3rd table to implement many-to-many relations, where the middle table holds the id from table A and the id from table B.

In this chapter, we will add skills for players.

In MongoDB, we can implement many-to-many relations using two-way embedded approaches and traditional approaches. MongoDB won't block us from using any one of these.

We add another model Skill alongside Country and Player to illustrate many-to-many relations.

Model definition

First, we will implement the traditional way of implementing many-to-many relations.

First, declare the Country, Skill, and Player models.

Then, declare the middle model PlayerSkill that will have player_id, skill_id, and rating fields.

# Code omitted above

class Country(Document):
    name: str


class Skill(Document):
    name: str


class Player(Document):
    name: str
    country_id: ODMObjectId

    country: Optional[Country] = Relationship(local_field="country_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel([("country_id", ASCENDING)]),
        ]


class PlayerSkill(Document):
    player_id: ODMObjectId
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)

    player: Optional[Player] = Relationship(local_field="player_id")
    skill: Optional[Skill] = Relationship(local_field="skill_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel(
                [("player_id", ASCENDING), ("skill_id", ASCENDING)],
                unique=True,
            ),
        ]

# Code omitted below
Full file preview
import os
from typing import Optional

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


class Country(Document):
    name: str


class Skill(Document):
    name: str


class Player(Document):
    name: str
    country_id: ODMObjectId

    country: Optional[Country] = Relationship(local_field="country_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel([("country_id", ASCENDING)]),
        ]


class PlayerSkill(Document):
    player_id: ODMObjectId
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)

    player: Optional[Player] = Relationship(local_field="player_id")
    skill: Optional[Skill] = Relationship(local_field="skill_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel(
                [("player_id", ASCENDING), ("skill_id", ASCENDING)],
                unique=True,
            ),
        ]


def insert_data():
    brazil = Country(name="Brazil").create()
    argentina = Country(name="Argentina").create()
    france = Country(name="France").create()

    catching = Skill(name="Catching")
    running = Skill(name="Running")
    kicking = Skill(name="Kicking")

    pele = Player(name="Pelé", country_id=brazil.id).create()
    PlayerSkill(player_id=pele.id, skill_id=catching.id, rating=49).create()
    PlayerSkill(player_id=pele.id, skill_id=kicking.id, rating=49).create()

    maradona = Player(name="Diego Maradona", country_id=argentina.id).create()
    PlayerSkill(player_id=maradona.id, skill_id=catching.id, rating=48).create()
    PlayerSkill(player_id=maradona.id, skill_id=kicking.id, rating=49).create()

    zidane = Player(name="Zinedine Zidane", country_id=france.id).create()
    PlayerSkill(player_id=zidane.id, skill_id=running.id, rating=42).create()
    PlayerSkill(player_id=zidane.id, skill_id=kicking.id, rating=42).create()


def read_data():
    pele = Player.get({Player.name: "Pelé"})
    skills = list(PlayerSkill.find({"player_id": pele.id}))

    print(pele)
    print(skills)


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


def main():
    configuration()
    insert_data()
    read_data()


if __name__ == "__main__":
    main()

Multi key indexes

We will define a multi-key index for better performance. Also, we want player_id and skill_id to be unique.

# Code omitted above

class PlayerSkill(Document):
    player_id: ODMObjectId
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)

    player: Optional[Player] = Relationship(local_field="player_id")
    skill: Optional[Skill] = Relationship(local_field="skill_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel(
                [("player_id", ASCENDING), ("skill_id", ASCENDING)],
                unique=True,
            ),
        ]

# Code omitted below

Here we use IndexModel, which is directly used from Pymongo.

Read data

We will get one of the player's data and his skills.

# Code omitted above


def read_data():
    pele = Player.get({Player.name: "Pelé"})
    skills = list(PlayerSkill.find({"player_id": pele.id}))

    print(pele)
    print(skills)

# Code omitted below
Full file preview
import os
from typing import Optional

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


class Country(Document):
    name: str


class Skill(Document):
    name: str


class Player(Document):
    name: str
    country_id: ODMObjectId

    country: Optional[Country] = Relationship(local_field="country_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel([("country_id", ASCENDING)]),
        ]


class PlayerSkill(Document):
    player_id: ODMObjectId
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)

    player: Optional[Player] = Relationship(local_field="player_id")
    skill: Optional[Skill] = Relationship(local_field="skill_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel(
                [("player_id", ASCENDING), ("skill_id", ASCENDING)],
                unique=True,
            ),
        ]


def insert_data():
    brazil = Country(name="Brazil").create()
    argentina = Country(name="Argentina").create()
    france = Country(name="France").create()

    catching = Skill(name="Catching")
    running = Skill(name="Running")
    kicking = Skill(name="Kicking")

    pele = Player(name="Pelé", country_id=brazil.id).create()
    PlayerSkill(player_id=pele.id, skill_id=catching.id, rating=49).create()
    PlayerSkill(player_id=pele.id, skill_id=kicking.id, rating=49).create()

    maradona = Player(name="Diego Maradona", country_id=argentina.id).create()
    PlayerSkill(player_id=maradona.id, skill_id=catching.id, rating=48).create()
    PlayerSkill(player_id=maradona.id, skill_id=kicking.id, rating=49).create()

    zidane = Player(name="Zinedine Zidane", country_id=france.id).create()
    PlayerSkill(player_id=zidane.id, skill_id=running.id, rating=42).create()
    PlayerSkill(player_id=zidane.id, skill_id=kicking.id, rating=42).create()


def read_data():
    pele = Player.get({Player.name: "Pelé"})
    skills = list(PlayerSkill.find({"player_id": pele.id}))

    print(pele)
    print(skills)


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


def main():
    configuration()
    insert_data()
    read_data()


if __name__ == "__main__":
    main()

After running the read_data function, the console should print:

Player(id=ObjectId('id'), name='Pelé', country_id=ObjectId('id'), _id=ObjectId('id'))

[PlayerSkill(id=ObjectId('id'), player_id=ObjectId('id'), skill_id=ObjectId('id'), rating=49, _id=ObjectId('id')), PlayerSkill(id=ObjectId('id'), player_id=ObjectId('id'), skill_id=ObjectId('id'), rating=49, _id=ObjectId('id'))]

Run Full Code

Run the code and check the MongoDB document viewer to see the impact.

import os
from typing import Optional

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


class Country(Document):
    name: str


class Skill(Document):
    name: str


class Player(Document):
    name: str
    country_id: ODMObjectId

    country: Optional[Country] = Relationship(local_field="country_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel([("country_id", ASCENDING)]),
        ]


class PlayerSkill(Document):
    player_id: ODMObjectId
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)

    player: Optional[Player] = Relationship(local_field="player_id")
    skill: Optional[Skill] = Relationship(local_field="skill_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel(
                [("player_id", ASCENDING), ("skill_id", ASCENDING)],
                unique=True,
            ),
        ]


def insert_data():
    brazil = Country(name="Brazil").create()
    argentina = Country(name="Argentina").create()
    france = Country(name="France").create()

    catching = Skill(name="Catching")
    running = Skill(name="Running")
    kicking = Skill(name="Kicking")

    pele = Player(name="Pelé", country_id=brazil.id).create()
    PlayerSkill(player_id=pele.id, skill_id=catching.id, rating=49).create()
    PlayerSkill(player_id=pele.id, skill_id=kicking.id, rating=49).create()

    maradona = Player(name="Diego Maradona", country_id=argentina.id).create()
    PlayerSkill(player_id=maradona.id, skill_id=catching.id, rating=48).create()
    PlayerSkill(player_id=maradona.id, skill_id=kicking.id, rating=49).create()

    zidane = Player(name="Zinedine Zidane", country_id=france.id).create()
    PlayerSkill(player_id=zidane.id, skill_id=running.id, rating=42).create()
    PlayerSkill(player_id=zidane.id, skill_id=kicking.id, rating=42).create()


def read_data():
    pele = Player.get({Player.name: "Pelé"})
    skills = list(PlayerSkill.find({"player_id": pele.id}))

    print(pele)
    print(skills)


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


def main():
    configuration()
    insert_data()
    read_data()


if __name__ == "__main__":
    main()

Model definition (Embedded way)

In this example, we use MongoDB's embedded approach to implement many-to-many relations.

# Code omitted above

class Country(Document):
    name: str


class Skill(Document):
    name: str


class EmbeddedSkill(BaseModel):
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)


class Player(Document):
    name: str
    country_id: ODMObjectId
    skills: list[EmbeddedSkill] = []

    country: Optional[Country] = Relationship(local_field="country_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel([("country_id", ASCENDING)]),
        ]

# Code omitted below
Full file preview
import os
from typing import Optional

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


class Country(Document):
    name: str


class Skill(Document):
    name: str


class EmbeddedSkill(BaseModel):
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)


class Player(Document):
    name: str
    country_id: ODMObjectId
    skills: list[EmbeddedSkill] = []

    country: Optional[Country] = Relationship(local_field="country_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel([("country_id", ASCENDING)]),
        ]


def insert_data():
    brazil = Country(name="Brazil").create()
    argentina = Country(name="Argentina").create()
    france = Country(name="France").create()

    catching = Skill(name="Catching")
    running = Skill(name="Running")
    kicking = Skill(name="Kicking")

    Player(
        name="Pelé",
        country_id=brazil.id,
        skills=[
            EmbeddedSkill(skill_id=catching.id, rating=49),
            EmbeddedSkill(skill_id=kicking.id, rating=49),
        ],
    ).create()
    Player(
        name="Diego Maradona",
        country_id=argentina.id,
        skills=[
            EmbeddedSkill(skill_id=catching.id, rating=48),
            EmbeddedSkill(skill_id=kicking.id, rating=49),
        ],
    ).create()
    Player(
        name="Zinedine Zidane",
        country_id=france.id,
        skills=[
            EmbeddedSkill(skill_id=running.id, rating=42),
            EmbeddedSkill(skill_id=kicking.id, rating=42),
        ],
    ).create()


def read_data():
    pele = Player.get({Player.name: "Pelé"})
    print(pele)


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


def main():
    configuration()
    insert_data()
    read_data()


if __name__ == "__main__":
    main()

First, we define the new model Skill.

Then we define the Pydantic model EmbeddedSkill. The name is not special and we can use any name we want.

The EmbeddedSkill has two fields: ODMObjectId type skill_id and int type rating.

We add the list[EmbeddedSkill] type skills field in the Player model.

Insert Data

First, create a country and skill document.

Then create a player with a list of EmbeddedSkill.

# Code omitted above

def insert_data():
    brazil = Country(name="Brazil").create()
    argentina = Country(name="Argentina").create()
    france = Country(name="France").create()

    catching = Skill(name="Catching")
    running = Skill(name="Running")
    kicking = Skill(name="Kicking")

    Player(
        name="Pelé",
        country_id=brazil.id,
        skills=[
            EmbeddedSkill(skill_id=catching.id, rating=49),
            EmbeddedSkill(skill_id=kicking.id, rating=49),
        ],
    ).create()
    Player(
        name="Diego Maradona",
        country_id=argentina.id,
        skills=[
            EmbeddedSkill(skill_id=catching.id, rating=48),
            EmbeddedSkill(skill_id=kicking.id, rating=49),
        ],
    ).create()
    Player(
        name="Zinedine Zidane",
        country_id=france.id,
        skills=[
            EmbeddedSkill(skill_id=running.id, rating=42),
            EmbeddedSkill(skill_id=kicking.id, rating=42),
        ],
    ).create()

# Code omitted below
Full file preview
import os
from typing import Optional

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


class Country(Document):
    name: str


class Skill(Document):
    name: str


class EmbeddedSkill(BaseModel):
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)


class Player(Document):
    name: str
    country_id: ODMObjectId
    skills: list[EmbeddedSkill] = []

    country: Optional[Country] = Relationship(local_field="country_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel([("country_id", ASCENDING)]),
        ]


def insert_data():
    brazil = Country(name="Brazil").create()
    argentina = Country(name="Argentina").create()
    france = Country(name="France").create()

    catching = Skill(name="Catching")
    running = Skill(name="Running")
    kicking = Skill(name="Kicking")

    Player(
        name="Pelé",
        country_id=brazil.id,
        skills=[
            EmbeddedSkill(skill_id=catching.id, rating=49),
            EmbeddedSkill(skill_id=kicking.id, rating=49),
        ],
    ).create()
    Player(
        name="Diego Maradona",
        country_id=argentina.id,
        skills=[
            EmbeddedSkill(skill_id=catching.id, rating=48),
            EmbeddedSkill(skill_id=kicking.id, rating=49),
        ],
    ).create()
    Player(
        name="Zinedine Zidane",
        country_id=france.id,
        skills=[
            EmbeddedSkill(skill_id=running.id, rating=42),
            EmbeddedSkill(skill_id=kicking.id, rating=42),
        ],
    ).create()


def read_data():
    pele = Player.get({Player.name: "Pelé"})
    print(pele)


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


def main():
    configuration()
    insert_data()
    read_data()


if __name__ == "__main__":
    main()

Read Data

Let's get one of the player's data.

# Code omitted above

def read_data():
    pele = Player.get({Player.name: "Pelé"})
    print(pele)

# Code omitted below
Full file preview
import os
from typing import Optional

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


class Country(Document):
    name: str


class Skill(Document):
    name: str


class Player(Document):
    name: str
    country_id: ODMObjectId

    country: Optional[Country] = Relationship(local_field="country_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel([("country_id", ASCENDING)]),
        ]


class PlayerSkill(Document):
    player_id: ODMObjectId
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)

    player: Optional[Player] = Relationship(local_field="player_id")
    skill: Optional[Skill] = Relationship(local_field="skill_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel(
                [("player_id", ASCENDING), ("skill_id", ASCENDING)],
                unique=True,
            ),
        ]


def insert_data():
    brazil = Country(name="Brazil").create()
    argentina = Country(name="Argentina").create()
    france = Country(name="France").create()

    catching = Skill(name="Catching")
    running = Skill(name="Running")
    kicking = Skill(name="Kicking")

    pele = Player(name="Pelé", country_id=brazil.id).create()
    PlayerSkill(player_id=pele.id, skill_id=catching.id, rating=49).create()
    PlayerSkill(player_id=pele.id, skill_id=kicking.id, rating=49).create()

    maradona = Player(name="Diego Maradona", country_id=argentina.id).create()
    PlayerSkill(player_id=maradona.id, skill_id=catching.id, rating=48).create()
    PlayerSkill(player_id=maradona.id, skill_id=kicking.id, rating=49).create()

    zidane = Player(name="Zinedine Zidane", country_id=france.id).create()
    PlayerSkill(player_id=zidane.id, skill_id=running.id, rating=42).create()
    PlayerSkill(player_id=zidane.id, skill_id=kicking.id, rating=42).create()


def read_data():
    pele = Player.get({Player.name: "Pelé"})
    skills = list(PlayerSkill.find({"player_id": pele.id}))

    print(pele)
    print(skills)


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


def main():
    configuration()
    insert_data()
    read_data()


if __name__ == "__main__":
    main()

After running the read_data function, the console should print:

Player(id=ObjectId('id'), name='Pelé', country_id=ObjectId('id'), skills=[EmbeddedSkill(skill_id=ObjectId('id'), rating=49), EmbeddedSkill(skill_id=ObjectId('id'), rating=49)], _id=ObjectId('id'))

Run Full Code

Run the code and check the MongoDB document viewer to see the impact.

import os
from typing import Optional

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


class Country(Document):
    name: str


class Skill(Document):
    name: str


class EmbeddedSkill(BaseModel):
    skill_id: ODMObjectId
    rating: int = Field(default=0, ge=1, le=100)


class Player(Document):
    name: str
    country_id: ODMObjectId
    skills: list[EmbeddedSkill] = []

    country: Optional[Country] = Relationship(local_field="country_id")

    class ODMConfig(Document.ODMConfig):
        indexes = [
            IndexModel([("country_id", ASCENDING)]),
        ]


def insert_data():
    brazil = Country(name="Brazil").create()
    argentina = Country(name="Argentina").create()
    france = Country(name="France").create()

    catching = Skill(name="Catching")
    running = Skill(name="Running")
    kicking = Skill(name="Kicking")

    Player(
        name="Pelé",
        country_id=brazil.id,
        skills=[
            EmbeddedSkill(skill_id=catching.id, rating=49),
            EmbeddedSkill(skill_id=kicking.id, rating=49),
        ],
    ).create()
    Player(
        name="Diego Maradona",
        country_id=argentina.id,
        skills=[
            EmbeddedSkill(skill_id=catching.id, rating=48),
            EmbeddedSkill(skill_id=kicking.id, rating=49),
        ],
    ).create()
    Player(
        name="Zinedine Zidane",
        country_id=france.id,
        skills=[
            EmbeddedSkill(skill_id=running.id, rating=42),
            EmbeddedSkill(skill_id=kicking.id, rating=42),
        ],
    ).create()


def read_data():
    pele = Player.get({Player.name: "Pelé"})
    print(pele)


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


def main():
    configuration()
    insert_data()
    read_data()


if __name__ == "__main__":
    main()