Skip to content
Closed
113 changes: 113 additions & 0 deletions docs/tutorial/relationship-attributes/aliased-relationships.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Aliased Relationships

## Multiple Relationships to the Same Model

We've seen how tables are related to each other via a single relationship attribute but what if more than
one attribute links to the same table?

What if you have a `User` model and an `Address` model and would like
to have `User.home_address` and `User.work_address` relationships to the same
`Address` model? In SQL you do this by creating a table alias using `AS` like this:

```
SELECT *
FROM user
JOIN address AS home_address_alias
ON user.home_address_id == home_address_alias.id
JOIN address AS work_address_alias
ON user.work_address_id == work_address_alias.id
```

The aliases we create are `home_address_alias` and `work_address_alias`. You can think of them
as a view to the same underlying `address` table.

We can do this with **SQLModel** and **SQLAlchemy** using `sqlalchemy.orm.aliased`
and a couple of extra bits of info in our **SQLModel** relationship definition and join statements.

## The Relationships

Let's define a `winter_team` and `summer_team` relationship for our heros. They can be on different
winter and summer teams or on the same team for both seasons.

```Python hl_lines="11 15"
# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py[ln:13-26]!}

# Code below omitted 👇
```

/// details | 👀 Full file preview

```Python
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py!}
```

///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
```Python hl_lines="11 15"
# Code above omitted 👆
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py[ln:13-26]!}
# Code below omitted 👇
```
/// details | 👀 Full file preview
```Python
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py!}
```
///
{* ./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py ln[13:26] hl[11,15] *}

The format of includes has been changed and now it's much simpler


The `sa_relationship_kwargs={"primaryjoin": ...}` is a new bit of info we need for **SQLAlchemy** to
figure out which SQL join we should use depending on which attribute is in our query.

## Creating Heros

Creating `Heros` with the multiple teams is no different from before. We set the same or different
team to the `winter_team` and `summer_team` attributes:


```Python hl_lines="11-12 18-19"
# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py[ln:39-65]!}

# Code below omitted 👇
```

/// details | 👀 Full file preview

```Python
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py!}
```

///
## Searching for Heros
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding a "Get hero's teams" section and show how easy it is to access Hero.winter_team and Hero.summer_team?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was marked as duplicate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant something like

hero = session.get(Hero, hero_id)
print(f"Hero: {hero.name}, Winter Team: {hero.winter_team.name} Summer Team: {hero.summer_team.name}")


Querying `Heros` based on the winter or summer teams adds a bit of complication. We need to create the
alias and we also need to be a bit more explicit in how we tell **SQLAlchemy** to join the `hero` and `team` tables.

We create the alias using `sqlalchemy.orm.aliased` function and use the alias in the `where` function. We also
need to provide an `onclause` argument to the `join`.

```Python hl_lines="3 8 9"
# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py[ln:70-79]!}

# Code below omitted 👇
```

/// details | 👀 Full file preview

```Python
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py!}
```

///
The value for the `onclause` is the same value that you used in the `primaryjoin` argument
when the relationship is defined in the `Hero` model.

To use both team attributes in a query, create another `alias` and add the join:

```Python hl_lines="3 9 10"
# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py[ln:82-95]!}

# Code below omitted 👇
```
/// details | 👀 Full file preview

```Python
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py!}
```

///
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from typing import Optional

from sqlalchemy.orm import aliased
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select


class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
headquarters: str


class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)

winter_team_id: Optional[int] = Field(default=None, foreign_key="team.id")
winter_team: Optional[Team] = Relationship(
sa_relationship_kwargs={"primaryjoin": "Hero.winter_team_id == Team.id"}
)
summer_team_id: Optional[int] = Field(default=None, foreign_key="team.id")
summer_team: Optional[Team] = Relationship(
sa_relationship_kwargs={"primaryjoin": "Hero.summer_team_id == Team.id"}
)


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

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


def create_heroes():
with Session(engine) as session:
team_preventers = Team(name="Preventers", headquarters="Sharp Tower")
team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar")

hero_deadpond = Hero(
name="Deadpond",
secret_name="Dive Wilson",
winter_team=team_preventers,
summer_team=team_z_force,
)
hero_rusty_man = Hero(
name="Rusty-Man",
secret_name="Tommy Sharp",
age=48,
winter_team=team_preventers,
summer_team=team_preventers,
)
session.add(hero_deadpond)
session.add(hero_rusty_man)
session.commit()

session.refresh(hero_deadpond)
session.refresh(hero_rusty_man)

print("Created hero:", hero_deadpond)
print("Created hero:", hero_rusty_man)


def select_heroes():
with Session(engine) as session:
winter_alias = aliased(Team)

# Heros with winter team as the Preventers
result = session.exec(
select(Hero)
.join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id)
.where(winter_alias.name == "Preventers")
)
heros = result.all()
print("Heros with Preventers as their winter team:", heros)
assert len(heros) == 2

summer_alias = aliased(Team)
# Heros with Preventers as their winter team and Z-Force as their summer team
result = session.exec(
select(Hero)
.join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id)
.where(winter_alias.name == "Preventers")
.join(summer_alias, onclause=Hero.summer_team_id == summer_alias.id)
.where(summer_alias.name == "Z-Force")
)
heros = result.all()
print(
"Heros with Preventers as their winter and Z-Force as their summer team:",
heros,
)
assert len(heros) == 1
assert heros[0].name == "Deadpond"


def main():
create_db_and_tables()
create_heroes()
select_heroes()


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from sqlalchemy.orm import aliased
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select


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


class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)

winter_team_id: int | None = Field(default=None, foreign_key="team.id")
winter_team: Team | None = Relationship(
sa_relationship_kwargs={"primaryjoin": "Hero.winter_team_id == Team.id"}
)
summer_team_id: int | None = Field(default=None, foreign_key="team.id")
summer_team: Team | None = Relationship(
sa_relationship_kwargs={"primaryjoin": "Hero.summer_team_id == Team.id"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more question - why did you choose to specify primaryjoin and not foreign_keys here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember. Our code was probably using "primaryjoin" for other things already. I'll give both a try.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like primaryjoin and foreign_keys work the same way in this case. I dug into the sqlalchemy docs a bit more and it looks like foreign_keys is the recommended way to to this: https://docs.sqlalchemy.org/en/20/orm/join_conditions.html#handling-multiple-join-paths.

)


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

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


def create_heroes():
with Session(engine) as session:
team_preventers = Team(name="Preventers", headquarters="Sharp Tower")
team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar")

hero_deadpond = Hero(
name="Deadpond",
secret_name="Dive Wilson",
winter_team=team_preventers,
summer_team=team_z_force,
)
hero_rusty_man = Hero(
name="Rusty-Man",
secret_name="Tommy Sharp",
age=48,
winter_team=team_preventers,
summer_team=team_preventers,
)
session.add(hero_deadpond)
session.add(hero_rusty_man)
session.commit()

session.refresh(hero_deadpond)
session.refresh(hero_rusty_man)

print("Created hero:", hero_deadpond)
print("Created hero:", hero_rusty_man)


def select_heroes():
with Session(engine) as session:
winter_alias = aliased(Team)

# Heros with winter team as the Preventers
result = session.exec(
select(Hero)
.join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id)
.where(winter_alias.name == "Preventers")
)
heros = result.all()
print("Heros with Preventers as their winter team:", heros)
assert len(heros) == 2

summer_alias = aliased(Team)

# Heros with Preventers as their winter team and Z-Force as their summer team
result = session.exec(
select(Hero)
.join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id)
.where(winter_alias.name == "Preventers")
.join(summer_alias, onclause=Hero.summer_team_id == summer_alias.id)
.where(summer_alias.name == "Z-Force")
)
heros = result.all()
print(
"Heros with Preventers as their winter and Z-Force as their summer team:",
heros,
)
assert len(heros) == 1
assert heros[0].name == "Deadpond"


def main():
create_db_and_tables()
create_heroes()
select_heroes()


if __name__ == "__main__":
main()
Loading