Skip to content

Commit da7c0c3

Browse files
committed
Tests and example for how to use alias.
1 parent 6b56235 commit da7c0c3

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 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/relationship_to_aliased_model/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/relationship_to_aliased_model/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/relationship_to_aliased_model/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/relationship_to_aliased_model/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/relationship_to_aliased_model/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/relationship_to_aliased_model/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/relationship_to_aliased_model/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/relationship_to_aliased_model/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)