-
-
Notifications
You must be signed in to change notification settings - Fork 781
Tests and example for how to use relationship alias. #815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
bd2d61a
Example for how to use alias with relationships.
cycledriver d5f78f2
Merge branch 'main' into docs/aliases
YuriiMotov 870dc4c
Merge branch 'fastapi:main' into docs/aliases
cycledriver fe0187c
Update docs/tutorial/relationship-attributes/aliased-relationships.md
cycledriver 10b79b9
Docs: multiple relationships to the same model-expand examples to sho…
cycledriver bf737bf
Docs: multiple relationships to to the same model-fix examples, add r…
cycledriver f419d74
Tests: fix tests after refactor
cycledriver dfdaf23
Tests: fix tests after refactor
cycledriver d514df2
Doocs: fix highlighting
cycledriver 1f80fcb
Doocs: fix highlighting
cycledriver a6e1492
Doocs: fix highlighting
cycledriver File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
174 changes: 174 additions & 0 deletions
174
docs/tutorial/relationship-attributes/multiple-relationships-same-model.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| # 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 one of two ways: 1) by creating a table alias using `AS` or 2) | ||
| by using a correlated sub-query. | ||
|
|
||
| Query: Find users with home address zipcode "100000" and work address zipcode = "10001": | ||
|
|
||
| ### Alias Query | ||
|
|
||
| Using an alias, JOIN the address table twice: | ||
| ```sql | ||
| 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 | ||
| WHERE | ||
| home_address_alias.zipcode == "10000" | ||
| AND work_address_alias.zipcode == "10001" | ||
| ``` | ||
|
|
||
| ### Correlated Sub-Query | ||
| Using sub-queries, filter the matches with EXISTS: | ||
| ```sql | ||
| SELECT * | ||
| FROM user | ||
| WHERE ( | ||
| EXISTS ( | ||
| SELECT 1 FROM address | ||
| WHERE | ||
| address.id = user.home_address_id | ||
| AND address.zipcode = "10000" | ||
| ) | ||
| ) AND ( | ||
| EXISTS ( | ||
| SELECT 1 FROM address | ||
| WHERE | ||
| address.id = user.work_address_id | ||
| AND address.zipcode = "10001" | ||
| ) | ||
|
|
||
| ``` | ||
|
|
||
| ### Key differences | ||
|
|
||
| Duplicates: JOIN (alias query) can produce them, EXISTS will not. The duplicates will be removed by the ORM | ||
| as rows are marshalled into objects. | ||
|
|
||
| Performance: Both can be optimized similarly, but JOIN often wins when you’re retrieving columns from the related table. | ||
|
|
||
| Readability: JOIN reads like “combine these tables.” EXISTS reads like “filter by a condition.” | ||
|
|
||
| ✅ Rule of thumb: | ||
|
|
||
| If you need columns from the foreign table → use JOIN. For example, if you are using `lazy=joined` or `selectin` you may prefer this. | ||
|
|
||
| If you only care whether a row exists in the foreign table → use EXISTS. | ||
|
|
||
| If the foreign table search criteria (address.zipcode) is not unique, prefer EXISTS unless you also want the duplicates. | ||
|
|
||
| ## 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/multiple_relationships_same_model/tutorial001.py[ln:13-26]!} | ||
|
|
||
| # Code below omitted 👇 | ||
| ``` | ||
|
|
||
| The `sa_relationship_kwargs={"foreign_keys": ...}` 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/multiple_relationships_same_model/tutorial001.py[ln:39-65]!} | ||
|
|
||
| # Code below omitted 👇 | ||
| ``` | ||
|
|
||
| /// details | 👀 Full file preview | ||
|
|
||
| ```Python | ||
| {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py!} | ||
| ``` | ||
|
|
||
| /// | ||
| ## Searching for Heros | ||
|
|
||
| Querying `Heros` based on the winter or summer teams adds a bit of complication. As | ||
| mentioned above, we can solve this with an aliased join or correlated subquery. | ||
|
|
||
| ### Alias Join | ||
|
|
||
| To use the alias method we need to: 1) create the alias(es) and 2) provide the join in our query. | ||
|
|
||
| #### Aliases | ||
|
|
||
| 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`. | ||
|
|
||
| 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** join statements. | ||
|
|
||
| ```Python hl_lines="4" | ||
| # Code above omitted 👆 | ||
|
|
||
| {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:69-71]!} | ||
|
|
||
| # Code below omitted 👇 | ||
| ``` | ||
|
|
||
| #### Join | ||
|
|
||
| Query Heros filtering by Team attributes by manually specifying the `join` with an `onclause` to tell **SQLAlchemy** to join the `hero` and `team` tables. | ||
|
|
||
| ```Python hl_lines="9" | ||
| # Code above omitted 👆 | ||
|
|
||
| {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:69-91]!} | ||
|
|
||
| # Code below omitted 👇 | ||
| ``` | ||
|
|
||
| The value for the `onclause` is the join using the same foreign key | ||
| when the relationship is defined in the `Hero` model. | ||
|
|
||
| To use both team attributes in a query, create another `alias` and add the join. | ||
|
|
||
| For more information see [SQLAlchemy: Handling Multiple Join Paths](https://docs.sqlalchemy.org/en/20/orm/join_conditions.html#handling-multiple-join-paths). | ||
|
|
||
| /// details | 👀 Full file preview | ||
|
|
||
| ```Python | ||
| {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py!} | ||
| ``` | ||
|
|
||
| /// | ||
|
|
||
| ### Correlated Sub Query | ||
|
|
||
| From a query perspecitve, this is a much simpler solution. We use the `has` function in the query: | ||
|
|
||
| ```Python hl_lines="6 7" | ||
| # Code above omitted 👆 | ||
|
|
||
| {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:93-123]!} | ||
|
|
||
| # Code below omitted 👇 | ||
| ``` | ||
| /// details | 👀 Full file preview | ||
|
|
||
| ```Python | ||
| {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py!} | ||
| ``` | ||
|
|
||
| /// | ||
Empty file.
134 changes: 134 additions & 0 deletions
134
docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| 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={"foreign_keys": "Hero.winter_team_id"} | ||
| ) | ||
| summer_team_id: Optional[int] = Field(default=None, foreign_key="team.id") | ||
| summer_team: Optional[Team] = Relationship( | ||
| sa_relationship_kwargs={"foreign_keys": "Hero.summer_team_id"} | ||
| ) | ||
|
|
||
|
|
||
| sqlite_file_name = ":memory:" | ||
| 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 using "aliases" and "onclause" | ||
| result = session.exec( | ||
| select(Hero) | ||
| .join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id) | ||
| .where(winter_alias.name == "Preventers") | ||
| ) | ||
| """ | ||
| SQL Looks like: | ||
|
|
||
| SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.winter_team_id, hero.summer_team_id | ||
| FROM hero JOIN team AS team_1 ON hero.winter_team_id = team_1.id | ||
| WHERE team_1.name = ? | ||
|
|
||
| """ | ||
| heros = result.all() | ||
| print("Heros with Preventers as their winter team:") | ||
| for hero in heros: | ||
| print( | ||
| f"Hero: {hero.name}, Winter Team: {hero.winter_team.name} Summer Team: {hero.summer_team.name}" | ||
| ) | ||
|
|
||
| # Heros with Preventers as their winter team and Z-Force as their summer team using "has" function. | ||
| result = session.exec( | ||
| select(Hero) | ||
| .where(Hero.winter_team.has(Team.name == "Preventers")) | ||
| .where(Hero.summer_team.has(Team.name == "Z-Force")) | ||
| ) | ||
| """ | ||
| SQL Looks like: | ||
|
|
||
| SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.winter_team_id, hero.summer_team_id | ||
| FROM hero | ||
| WHERE ( | ||
| EXISTS ( | ||
| SELECT 1 FROM team | ||
| WHERE team.id = hero.winter_team_id AND team.name = ? | ||
| ) | ||
| ) AND ( | ||
| EXISTS ( | ||
| SELECT 1 FROM team | ||
| WHERE team.id = hero.summer_team_id AND team.name = ? | ||
| ) | ||
| ) | ||
| """ | ||
| heros = result.all() | ||
| print( | ||
| "Heros with Preventers as their winter and Z-Force as their summer team:", | ||
| ) | ||
| for hero in heros: | ||
| print( | ||
| f"Hero: {hero.name}, Winter Team: {hero.winter_team.name} Summer Team: {hero.summer_team.name}" | ||
| ) | ||
| assert heros[0].name == "Deadpond" | ||
|
|
||
|
|
||
| def main(): | ||
| create_db_and_tables() | ||
| create_heroes() | ||
| select_heroes() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use new code include format: