Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5b4c9e4
📝 Add docs page for self-referential model
daniil-berg Aug 25, 2022
c702e78
Merge branch 'main' into main
tiangolo Dec 16, 2022
04eb680
Merge branch 'main' into main
YuriiMotov Aug 25, 2025
7065c7b
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Aug 25, 2025
d0bd01f
Fix Ruff C408 Unnecessary `dict()`
YuriiMotov Aug 25, 2025
cfa3541
Fix formatting and update links
YuriiMotov Aug 25, 2025
aac3497
Fix include and highlight intervals
YuriiMotov Aug 25, 2025
b399813
Fix include and highlight intervals 2
YuriiMotov Aug 25, 2025
4016556
Update issue-manager.yml
YuriiMotov Aug 28, 2025
7e45ae3
Apply Yurii's rephrasing suggestions
svlandeg Sep 17, 2025
95df345
Move to `advanced-relationships`, fix links
YuriiMotov Oct 8, 2025
daf3975
Some text improvements
YuriiMotov Oct 8, 2025
7463f0c
Merge branch 'main' into main
YuriiMotov Oct 8, 2025
a8a4cea
Revert "Update issue-manager.yml"
YuriiMotov Oct 8, 2025
c5de013
Apply suggestions from code review
YuriiMotov Oct 21, 2025
2bc70be
Merge branch 'main' into main
YuriiMotov Oct 21, 2025
ec46cdf
Merge remote-tracking branch 'upstream/main' into daniil-berg_main
YuriiMotov Jan 8, 2026
53809d1
Update code example to Python 3.9 syntax
YuriiMotov Jan 8, 2026
8ca82d4
Merge remote-tracking branch 'upstream/main' into daniil-berg_main
YuriiMotov May 18, 2026
f7b3e0a
Add `advanced/advanced-relationships/index.md` with short description
YuriiMotov May 18, 2026
fad68f4
🎨 Auto format
pre-commit-ci-lite[bot] May 18, 2026
115a3ba
Remove python 3.9 syntax and fix admonitions
YuriiMotov May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/advanced/advanced-relationships/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Advanced Relationships

The [Relationship Attributes](../../tutorial/relationship-attributes/index.md){.internal-link target=_blank} section describes the basics of relationships. Here we will cover some more advanced patterns.
78 changes: 78 additions & 0 deletions docs/advanced/advanced-relationships/self-referential.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Self-referential relationships

Oftentimes we need to model a relationship between one entity of some class and another entity (or multiple entities) of that **same** class. This is known as a **self-referential** or **recursive** relationship, sometimes also called an **adjacency list**.

In database terms this means having a table with a foreign key reference to the primary key in the same table.

Say, for example, we want to introduce a `Villain` class. 😈 Every villain can have a **boss**, who also must be a villain. If a villain is the boss of other villains, we want to call those his **minions**.

Let's implement this with **SQLModel**. 🤓

## Using SQLAlchemy arguments

We already learned a lot about [Relationship attributes](../../tutorial/relationship-attributes/index.md){.internal-link target=_blank} in previous chapters. We know that **SQLModel** is built on top of **SQLAlchemy**, which supports defining self-referential relationships (see [their documentation](https://docs.sqlalchemy.org/en/20/orm/self_referential.html){.external-link target=_blank}).

To allow more fine-grained control over it, the `Relationship` constructor allows explicitly passing additional keyword-arguments to the [`sqlalchemy.orm.relationship`](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship){.external-link target=_blank} constructor that is being called under the hood via the `sa_relationship_kwargs` parameter. This should be a mapping (e.g. a dictionary) of strings representing the SQLAlchemy **parameter names** to the **values** we want to pass through as arguments.

Since SQLAlchemy relationships provide the [`remote_side`](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship.params.remote_side){.external-link target=_blank} parameter for just such an occasion, we can leverage that directly to construct the self-referential pattern with minimal code.

{* ./docs_src/advanced/self_referential/tutorial001_py310.py ln[6:16] hl[14] *}

Using the `sa_relationship_kwargs` parameter, we pass the keyword argument `remote_side='Villain.id'` to the underlying relationship property.

/// note

The `remote_side` parameter accepts a Python-evaluable string when using Declarative. This allows us to reference `Villain.id` even though the class is still being defined.

Alternatively, you can use a callable:

```py
sa_relationship_kwargs={"remote_side": lambda : Villain.id}
```

///

## Back-populating and self-referencing

Notice that we explicitly defined the relationship attributes we wanted for referring to the `boss` **as well as** the `minions` of a `Villain`.

For our purposes, it is necessary that we also provide the `back_populates` parameter to both relationships as explained in detail in a [dedicated chapter](../../tutorial/relationship-attributes/back-populates.md){.internal-link target=_blank}.

In addition, the type annotations were made by enclosing our `Villain` class name in quotes, since we are referencing a class that is not yet fully defined by the time the interpreter reaches those lines. See the chapter on [type annotation strings](../../tutorial/relationship-attributes/type-annotation-strings.md){.internal-link target=_blank} for a detailed explanation.

Finally, as with regular (i.e. non-self-referential) foreign key relationships, it is up to us to decide whether it makes sense to allow the field to be **empty** or not. In our example, not every villain must have a boss (in fact, we would otherwise introduce a circular reference chain, which would not make sense in this context). Therefore we declare `boss_id: int | None` and `boss: 'Villain' | None`. This is analogous to the `Hero`→`Team` relationship we saw [in an earlier chapter](../../tutorial/relationship-attributes/define-relationships-attributes.md#relationship-attributes-or-none){.internal-link target=_blank}.

## Creating instances

Now let's see how we can create villains with a boss:

{* ./docs_src/advanced/self_referential/tutorial001_py310.py ln[29:48] hl[32:33] *}

Just as with regular relationships, we can simply pass our boss villain as an argument to the constructor using `boss=thinnus`.

If we later learn that a villain actually had a secret boss after we've already created him, we can just as easily assign that boss retroactively:

{* ./docs_src/advanced/self_referential/tutorial001_py310.py ln[29:30,50:54] hl[50] *}

And if we want to add minions to a boss afterward, it's as easy as adding items to a Python list (because that's all it is 🤓):

{* ./docs_src/advanced/self_referential/tutorial001_py310.py ln[29:30,56:67] hl[59] *}

Since our relationships work both ways, we don't even need to add all our `clone_bot_`s to the session individually. Instead, we can simply add `ultra_bot` again and commit the changes. We do need to refresh them individually, though, if we want to access their updated attributes.

## Traversing the relationship graph

By setting up our relationships this way, we can easily go back and forth along the graph representing all the relationships we've created so far.

For example, we can verify that our `clone_bot_1` has a boss, who has his own boss, and that one of that top boss's minions is `ebonite_mew`:

```Python
top_boss_minions = clone_bot_3.boss.boss.minions
assert any(minion is ebonite_mew for minion in top_boss_minions) # passes
```

/// note

Notice that we can, in fact, check for **identity** using `is` instead of `==` here, since we are dealing with the exact same objects, not just objects containing the same **data**.

///
Empty file.
76 changes: 76 additions & 0 deletions docs_src/advanced/self_referential/tutorial001_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from typing import Optional

from sqlmodel import Field, Relationship, Session, SQLModel, create_engine


class Villain(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
power_level: int

boss_id: int | None = Field(foreign_key="villain.id", default=None, nullable=True)
boss: Optional["Villain"] = Relationship(
back_populates="minions",
sa_relationship_kwargs={"remote_side": "Villain.id"},
)
minions: list["Villain"] = Relationship(back_populates="boss")


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=False)


def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)


def create_villains() -> None:
with Session(engine) as session:
thinnus = Villain(name="Thinnus", power_level=9001)
ebonite_mew = Villain(name="Ebonite Mew", power_level=400, boss=thinnus)
dark_shorty = Villain(name="Dark Shorty", power_level=200, boss=thinnus)
ultra_bot = Villain(name="Ultra Bot", power_level=2**9)
session.add(ebonite_mew)
session.add(dark_shorty)
session.add(ultra_bot)
session.commit()

session.refresh(thinnus)
session.refresh(ebonite_mew)
session.refresh(dark_shorty)
session.refresh(ultra_bot)

print("Created villain:", thinnus)
print("Created villain:", ebonite_mew)
print("Created villain:", dark_shorty)
print("Created villain:", ultra_bot)

ultra_bot.boss = thinnus
session.add(ultra_bot)
session.commit()
session.refresh(ultra_bot)
print("Updated villain:", ultra_bot)

clone_bot_1 = Villain(name="Clone Bot 1", power_level=2**6)
clone_bot_2 = Villain(name="Clone Bot 2", power_level=2**6)
clone_bot_3 = Villain(name="Clone Bot 3", power_level=2**6)
ultra_bot.minions.extend([clone_bot_1, clone_bot_2, clone_bot_3])
session.add(ultra_bot)
session.commit()
session.refresh(clone_bot_1)
session.refresh(clone_bot_2)
session.refresh(clone_bot_3)
print("Added minion:", clone_bot_1)
print("Added minion:", clone_bot_2)
print("Added minion:", clone_bot_3)


def main() -> None:
create_db_and_tables()
create_villains()


if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ nav:
- advanced/index.md
- advanced/decimal.md
- advanced/uuid.md
- "":
- advanced/advanced-relationships/index.md
- advanced/advanced-relationships/self-referential.md
- "":
- resources/index.md
- help.md
Expand Down
Empty file.
94 changes: 94 additions & 0 deletions tests/test_advanced/test_self_referential/test_tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from unittest.mock import patch

from sqlmodel import create_engine

from ...conftest import get_testing_print_function

expected_calls = [
[
"Created villain:",
{
"name": "Thinnus",
"power_level": 9001,
"id": 1,
"boss_id": None,
},
],
[
"Created villain:",
{
"name": "Ebonite Mew",
"power_level": 400,
"id": 3,
"boss_id": 1,
},
],
[
"Created villain:",
{
"name": "Dark Shorty",
"power_level": 200,
"id": 4,
"boss_id": 1,
},
],
[
"Created villain:",
{
"name": "Ultra Bot",
"power_level": 2**9,
"id": 2,
"boss_id": None,
},
],
[
"Updated villain:",
{
"name": "Ultra Bot",
"power_level": 2**9,
"id": 2,
"boss_id": 1,
},
],
[
"Added minion:",
{
"name": "Clone Bot 1",
"power_level": 2**6,
"id": 5,
"boss_id": 2,
},
],
[
"Added minion:",
{
"name": "Clone Bot 2",
"power_level": 2**6,
"id": 6,
"boss_id": 2,
},
],
[
"Added minion:",
{
"name": "Clone Bot 3",
"power_level": 2**6,
"id": 7,
"boss_id": 2,
},
],
]


def test_tutorial(clear_sqlmodel):
from docs_src.advanced.self_referential import tutorial001_py310 as mod

mod.sqlite_url = "sqlite://"
mod.engine = create_engine(mod.sqlite_url)
calls = []

new_print = get_testing_print_function(calls)

with patch("builtins.print", new=new_print):
mod.main()
assert calls == expected_calls
Loading