Skip to content

Commit bd2d61a

Browse files
committed
Example for how to use alias with relationships.
When one object has more than one relationship to the same foreign object, you need to use `aliased` to differentiate between the relationships.
1 parent 6b56235 commit bd2d61a

File tree

11 files changed

+739
-0
lines changed

11 files changed

+739
-0
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Aliased Relationships
2+
3+
## Multiple Relationships to the Same Model
4+
5+
We've seen how tables are related to each other via a single relationship attribute but what if more than
6+
one attribute links to the same table?
7+
8+
What if you have a `User` model and an `Address` model and would like
9+
to have `User.home_address` and `User.work_address` relationships to the same
10+
`Address` model? In SQL you do this by creating a table alias using `AS` like this:
11+
12+
```
13+
SELECT *
14+
FROM user
15+
JOIN address AS home_address_alias
16+
ON user.home_address_id == home_address_alias.id
17+
JOIN address AS work_address_alias
18+
ON user.work_address_id == work_address_alias.id
19+
```
20+
21+
The aliases we create are `home_address_alias` and `work_address_alias`. You can think of them
22+
as a view to the same underlying `address` table.
23+
24+
We can do this with **SQLModel** and **SQLAlchemy** using `sqlalchemy.orm.aliased`
25+
and a couple of extra bits of info in our **SQLModel** relationship definition and join statements.
26+
27+
## The Relationships
28+
29+
Let's define a `winter_team` and `summer_team` relationship for our heros. They can be on different
30+
winter and summer teams or on the same team for both seasons.
31+
32+
```Python hl_lines="11 15"
33+
# Code above omitted 👆
34+
35+
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py[ln:13-26]!}
36+
37+
# Code below omitted 👇
38+
```
39+
40+
/// details | 👀 Full file preview
41+
42+
```Python
43+
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py!}
44+
```
45+
46+
///
47+
48+
The `sa_relationship_kwargs={"primaryjoin": ...}` is a new bit of info we need for **SQLAlchemy** to
49+
figure out which SQL join we should use depending on which attribute is in our query.
50+
51+
## Creating Heros
52+
53+
Creating `Heros` with the multiple teams is no different from before. We set the same or different
54+
team to the `winter_team` and `summer_team` attributes:
55+
56+
57+
```Python hl_lines="11-12 18-19"
58+
# Code above omitted 👆
59+
60+
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py[ln:39-65]!}
61+
62+
# Code below omitted 👇
63+
```
64+
65+
/// details | 👀 Full file preview
66+
67+
```Python
68+
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py!}
69+
```
70+
71+
///
72+
## Searching for Heros
73+
74+
Querying `Heros` based on the winter or summer teams adds a bit of complication. We need to create the
75+
alias and we also need to be a bit more explicit in how we tell **SQLAlchemy** to join the `hero` and `team` tables.
76+
77+
We create the alias using `sqlalchemy.orm.aliased` function and use the alias in the `where` function. We also
78+
need to provide an `onclause` argument to the `join`.
79+
80+
```Python hl_lines="3 8 9"
81+
# Code above omitted 👆
82+
83+
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py[ln:70-79]!}
84+
85+
# Code below omitted 👇
86+
```
87+
88+
/// details | 👀 Full file preview
89+
90+
```Python
91+
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py!}
92+
```
93+
94+
///
95+
The value for the `onclause` is the same value that you used in the `primaryjoin` argument
96+
when the relationship is defined in the `Hero` model.
97+
98+
To use both team attributes in a query, create another `alias` and add the join:
99+
100+
```Python hl_lines="3 9 10"
101+
# Code above omitted 👆
102+
103+
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py[ln:82-95]!}
104+
105+
# Code below omitted 👇
106+
```
107+
/// details | 👀 Full file preview
108+
109+
```Python
110+
{!./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py!}
111+
```
112+
113+
///

docs_src/tutorial/relationship_attributes/aliased_relationship/__init__.py

Whitespace-only changes.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from typing import Optional
2+
3+
from sqlalchemy.orm import aliased
4+
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
5+
6+
7+
class Team(SQLModel, table=True):
8+
id: Optional[int] = Field(default=None, primary_key=True)
9+
name: str = Field(index=True)
10+
headquarters: str
11+
12+
13+
class Hero(SQLModel, table=True):
14+
id: Optional[int] = Field(default=None, primary_key=True)
15+
name: str = Field(index=True)
16+
secret_name: str
17+
age: Optional[int] = Field(default=None, index=True)
18+
19+
winter_team_id: Optional[int] = Field(default=None, foreign_key="team.id")
20+
winter_team: Optional[Team] = Relationship(
21+
sa_relationship_kwargs={"primaryjoin": "Hero.winter_team_id == Team.id"}
22+
)
23+
summer_team_id: Optional[int] = Field(default=None, foreign_key="team.id")
24+
summer_team: Optional[Team] = Relationship(
25+
sa_relationship_kwargs={"primaryjoin": "Hero.summer_team_id == Team.id"}
26+
)
27+
28+
29+
sqlite_file_name = "database.db"
30+
sqlite_url = f"sqlite:///{sqlite_file_name}"
31+
32+
engine = create_engine(sqlite_url, echo=True)
33+
34+
35+
def create_db_and_tables():
36+
SQLModel.metadata.create_all(engine)
37+
38+
39+
def create_heroes():
40+
with Session(engine) as session:
41+
team_preventers = Team(name="Preventers", headquarters="Sharp Tower")
42+
team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar")
43+
44+
hero_deadpond = Hero(
45+
name="Deadpond",
46+
secret_name="Dive Wilson",
47+
winter_team=team_preventers,
48+
summer_team=team_z_force,
49+
)
50+
hero_rusty_man = Hero(
51+
name="Rusty-Man",
52+
secret_name="Tommy Sharp",
53+
age=48,
54+
winter_team=team_preventers,
55+
summer_team=team_preventers,
56+
)
57+
session.add(hero_deadpond)
58+
session.add(hero_rusty_man)
59+
session.commit()
60+
61+
session.refresh(hero_deadpond)
62+
session.refresh(hero_rusty_man)
63+
64+
print("Created hero:", hero_deadpond)
65+
print("Created hero:", hero_rusty_man)
66+
67+
68+
def select_heroes():
69+
with Session(engine) as session:
70+
winter_alias = aliased(Team)
71+
72+
# Heros with winter team as the Preventers
73+
result = session.exec(
74+
select(Hero)
75+
.join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id)
76+
.where(winter_alias.name == "Preventers")
77+
)
78+
heros = result.all()
79+
print("Heros with Preventers as their winter team:", heros)
80+
assert len(heros) == 2
81+
82+
summer_alias = aliased(Team)
83+
# Heros with Preventers as their winter team and Z-Force as their summer team
84+
result = session.exec(
85+
select(Hero)
86+
.join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id)
87+
.where(winter_alias.name == "Preventers")
88+
.join(summer_alias, onclause=Hero.summer_team_id == summer_alias.id)
89+
.where(summer_alias.name == "Z-Force")
90+
)
91+
heros = result.all()
92+
print(
93+
"Heros with Preventers as their winter and Z-Force as their summer team:",
94+
heros,
95+
)
96+
assert len(heros) == 1
97+
assert heros[0].name == "Deadpond"
98+
99+
100+
def main():
101+
create_db_and_tables()
102+
create_heroes()
103+
select_heroes()
104+
105+
106+
if __name__ == "__main__":
107+
main()
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from sqlalchemy.orm import aliased
2+
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
3+
4+
5+
class Team(SQLModel, table=True):
6+
id: int | None = Field(default=None, primary_key=True)
7+
name: str = Field(index=True)
8+
headquarters: str
9+
10+
11+
class Hero(SQLModel, table=True):
12+
id: int | None = Field(default=None, primary_key=True)
13+
name: str = Field(index=True)
14+
secret_name: str
15+
age: int | None = Field(default=None, index=True)
16+
17+
winter_team_id: int | None = Field(default=None, foreign_key="team.id")
18+
winter_team: Team | None = Relationship(
19+
sa_relationship_kwargs={"primaryjoin": "Hero.winter_team_id == Team.id"}
20+
)
21+
summer_team_id: int | None = Field(default=None, foreign_key="team.id")
22+
summer_team: Team | None = Relationship(
23+
sa_relationship_kwargs={"primaryjoin": "Hero.summer_team_id == Team.id"}
24+
)
25+
26+
27+
sqlite_file_name = "database.db"
28+
sqlite_url = f"sqlite:///{sqlite_file_name}"
29+
30+
engine = create_engine(sqlite_url, echo=True)
31+
32+
33+
def create_db_and_tables():
34+
SQLModel.metadata.create_all(engine)
35+
36+
37+
def create_heroes():
38+
with Session(engine) as session:
39+
team_preventers = Team(name="Preventers", headquarters="Sharp Tower")
40+
team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar")
41+
42+
hero_deadpond = Hero(
43+
name="Deadpond",
44+
secret_name="Dive Wilson",
45+
winter_team=team_preventers,
46+
summer_team=team_z_force,
47+
)
48+
hero_rusty_man = Hero(
49+
name="Rusty-Man",
50+
secret_name="Tommy Sharp",
51+
age=48,
52+
winter_team=team_preventers,
53+
summer_team=team_preventers,
54+
)
55+
session.add(hero_deadpond)
56+
session.add(hero_rusty_man)
57+
session.commit()
58+
59+
session.refresh(hero_deadpond)
60+
session.refresh(hero_rusty_man)
61+
62+
print("Created hero:", hero_deadpond)
63+
print("Created hero:", hero_rusty_man)
64+
65+
66+
def select_heroes():
67+
with Session(engine) as session:
68+
winter_alias = aliased(Team)
69+
70+
# Heros with winter team as the Preventers
71+
result = session.exec(
72+
select(Hero)
73+
.join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id)
74+
.where(winter_alias.name == "Preventers")
75+
)
76+
heros = result.all()
77+
print("Heros with Preventers as their winter team:", heros)
78+
assert len(heros) == 2
79+
80+
summer_alias = aliased(Team)
81+
82+
# Heros with Preventers as their winter team and Z-Force as their summer team
83+
result = session.exec(
84+
select(Hero)
85+
.join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id)
86+
.where(winter_alias.name == "Preventers")
87+
.join(summer_alias, onclause=Hero.summer_team_id == summer_alias.id)
88+
.where(summer_alias.name == "Z-Force")
89+
)
90+
heros = result.all()
91+
print(
92+
"Heros with Preventers as their winter and Z-Force as their summer team:",
93+
heros,
94+
)
95+
assert len(heros) == 1
96+
assert heros[0].name == "Deadpond"
97+
98+
99+
def main():
100+
create_db_and_tables()
101+
create_heroes()
102+
select_heroes()
103+
104+
105+
if __name__ == "__main__":
106+
main()

0 commit comments

Comments
 (0)