From bd2d61a2e28f3b4a711a7dfea5df0f373d00155e Mon Sep 17 00:00:00 2001 From: Cycle Driver <66476455+cycledriver@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:26:39 -0500 Subject: [PATCH 1/9] 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. --- .../aliased-relationships.md | 113 ++++++++++++++++++ .../aliased_relationship/__init__.py | 0 .../aliased_relationship/tutorial001.py | 107 +++++++++++++++++ .../aliased_relationship/tutorial001_py310.py | 106 ++++++++++++++++ .../aliased_relationship/tutorial001_py39.py | 108 +++++++++++++++++ mkdocs.yml | 1 + tests/test_field_sa_relationship_alias.py | 62 ++++++++++ .../test_aliased_relationship/__init__.py | 0 .../test_tutorial001.py | 80 +++++++++++++ .../test_tutorial001_py310.py | 81 +++++++++++++ .../test_tutorial001_py39.py | 81 +++++++++++++ 11 files changed, 739 insertions(+) create mode 100644 docs/tutorial/relationship-attributes/aliased-relationships.md create mode 100644 docs_src/tutorial/relationship_attributes/aliased_relationship/__init__.py create mode 100644 docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py create mode 100644 docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py39.py create mode 100644 tests/test_field_sa_relationship_alias.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/__init__.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py39.py diff --git a/docs/tutorial/relationship-attributes/aliased-relationships.md b/docs/tutorial/relationship-attributes/aliased-relationships.md new file mode 100644 index 0000000000..b55618617d --- /dev/null +++ b/docs/tutorial/relationship-attributes/aliased-relationships.md @@ -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!} +``` + +/// + +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 + +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!} +``` + +/// diff --git a/docs_src/tutorial/relationship_attributes/aliased_relationship/__init__.py b/docs_src/tutorial/relationship_attributes/aliased_relationship/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py b/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py new file mode 100644 index 0000000000..6b3172316a --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py @@ -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() diff --git a/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py new file mode 100644 index 0000000000..7e08522a36 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py @@ -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"} + ) + + +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() diff --git a/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py39.py new file mode 100644 index 0000000000..c71d1b898d --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py39.py @@ -0,0 +1,108 @@ +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() diff --git a/mkdocs.yml b/mkdocs.yml index fa85062a8b..1e0d891223 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,6 +74,7 @@ nav: - tutorial/relationship-attributes/read-relationships.md - tutorial/relationship-attributes/remove-relationships.md - tutorial/relationship-attributes/back-populates.md + - tutorial/relationship-attributes/aliased-relationships.md - tutorial/relationship-attributes/type-annotation-strings.md - Many to Many: - tutorial/many-to-many/index.md diff --git a/tests/test_field_sa_relationship_alias.py b/tests/test_field_sa_relationship_alias.py new file mode 100644 index 0000000000..262e87df83 --- /dev/null +++ b/tests/test_field_sa_relationship_alias.py @@ -0,0 +1,62 @@ +from typing import Optional + +from sqlalchemy import create_engine +from sqlalchemy.orm import aliased +from sqlmodel import Field, Relationship, Session, SQLModel, select + + +def test_sa_multi_relationship_alias(clear_sqlmodel) -> None: + class Team(SQLModel, table=True): + """Team model.""" + + __tablename__ = "team_multi" + id: Optional[int] = Field(default=None, primary_key=True) + name: str + + class Hero(SQLModel, table=True): + """Hero model.""" + + __tablename__ = "hero_multi" + id: Optional[int] = Field(default=None, primary_key=True) + name: str + winter_team_id: Optional[int] = Field(default=None, foreign_key="team_multi.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_multi.id") + summer_team: Optional[Team] = Relationship( + sa_relationship_kwargs={"primaryjoin": "Hero.summer_team_id == Team.id"} + ) + + engine = create_engine("sqlite://", echo=True) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + blue_team = Team(name="Blue") + red_team = Team(name="Red") + session.add_all([blue_team, red_team]) + session.commit() + session.refresh(blue_team) + session.refresh(red_team) + + hero1 = Hero( + name="dual_team", winter_team_id=blue_team.id, summer_team_id=red_team.id + ) + session.add(hero1) + hero2 = Hero( + name="single_team", winter_team_id=red_team.id, summer_team_id=red_team.id + ) + session.add_all([hero1, hero2]) + session.commit() + + winter_alias = aliased(Team) + summer_alias = aliased(Team) + result = session.exec( + select(Hero) + .join(winter_alias, onclause=Hero.winter_team_id == winter_alias.id) + .where(winter_alias.name == "Blue") + .join(summer_alias, onclause=Hero.summer_team_id == summer_alias.id) + .where(summer_alias.name == "Red") + ).all() + assert len(result) == 1 + assert result[0].name == "dual_team" diff --git a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/__init__.py b/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001.py new file mode 100644 index 0000000000..f62a65187a --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001.py @@ -0,0 +1,80 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "summer_team_id": 1, + "winter_team_id": 1, + }, + ], + [ + "Heros with Preventers as their winter team:", + [ + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + }, + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "summer_team_id": 1, + "winter_team_id": 1, + }, + ], + ], + [ + "Heros with Preventers as their winter and Z-Force as their summer team:", + [ + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + } + ], + ], +] + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.aliased_relationship import ( + tutorial001 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 diff --git a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py310.py new file mode 100644 index 0000000000..002512afa9 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py310.py @@ -0,0 +1,81 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "summer_team_id": 1, + "winter_team_id": 1, + }, + ], + [ + "Heros with Preventers as their winter team:", + [ + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + }, + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "summer_team_id": 1, + "winter_team_id": 1, + }, + ], + ], + [ + "Heros with Preventers as their winter and Z-Force as their summer team:", + [ + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + } + ], + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.aliased_relationship 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 diff --git a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py39.py new file mode 100644 index 0000000000..2acf294ee6 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py39.py @@ -0,0 +1,81 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "summer_team_id": 1, + "winter_team_id": 1, + }, + ], + [ + "Heros with Preventers as their winter team:", + [ + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + }, + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "summer_team_id": 1, + "winter_team_id": 1, + }, + ], + ], + [ + "Heros with Preventers as their winter and Z-Force as their summer team:", + [ + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + } + ], + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.aliased_relationship import ( + tutorial001_py39 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 From fe0187c8deebe2d81eec4420efa88c9f77898ab6 Mon Sep 17 00:00:00 2001 From: cycledriver <66476455+cycledriver@users.noreply.github.com> Date: Sun, 31 Aug 2025 12:45:46 -0400 Subject: [PATCH 2/9] Update docs/tutorial/relationship-attributes/aliased-relationships.md Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- .../aliased-relationships.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/docs/tutorial/relationship-attributes/aliased-relationships.md b/docs/tutorial/relationship-attributes/aliased-relationships.md index b55618617d..b41d420df2 100644 --- a/docs/tutorial/relationship-attributes/aliased-relationships.md +++ b/docs/tutorial/relationship-attributes/aliased-relationships.md @@ -29,21 +29,7 @@ and a couple of extra bits of info in our **SQLModel** relationship definition a 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!} -``` - -/// +{* ./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py ln[13:26] hl[11,15] *} 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. From 10b79b9bcebf7d5a184d60727d4e3e27aa94c68b Mon Sep 17 00:00:00 2001 From: Cycle Driver <66476455+cycledriver@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:04:48 -0400 Subject: [PATCH 3/9] Docs: multiple relationships to the same model-expand examples to show multiple solutions --- .../aliased-relationships.md | 99 ----------- .../multiple-relationships-same-model.md | 168 ++++++++++++++++++ .../__init__.py | 0 .../tutorial001.py | 47 +++-- .../tutorial001_py310.py | 43 +++-- .../tutorial001_py39.py | 43 +++-- mkdocs.yml | 2 +- 7 files changed, 269 insertions(+), 133 deletions(-) delete mode 100644 docs/tutorial/relationship-attributes/aliased-relationships.md create mode 100644 docs/tutorial/relationship-attributes/multiple-relationships-same-model.md rename docs_src/tutorial/relationship_attributes/{aliased_relationship => multiple_relationships_same_model}/__init__.py (100%) rename docs_src/tutorial/relationship_attributes/{aliased_relationship => multiple_relationships_same_model}/tutorial001.py (67%) rename docs_src/tutorial/relationship_attributes/{aliased_relationship => multiple_relationships_same_model}/tutorial001_py310.py (70%) rename docs_src/tutorial/relationship_attributes/{aliased_relationship => multiple_relationships_same_model}/tutorial001_py39.py (70%) diff --git a/docs/tutorial/relationship-attributes/aliased-relationships.md b/docs/tutorial/relationship-attributes/aliased-relationships.md deleted file mode 100644 index b41d420df2..0000000000 --- a/docs/tutorial/relationship-attributes/aliased-relationships.md +++ /dev/null @@ -1,99 +0,0 @@ -# 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. - -{* ./docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py ln[13:26] hl[11,15] *} - -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 - -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!} -``` - -/// diff --git a/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md new file mode 100644 index 0000000000..70a2a8a9a6 --- /dev/null +++ b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md @@ -0,0 +1,168 @@ +# 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: +``` +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: +``` +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. + +{* ./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py ln[13:26] hl[11,15] *} + +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="2" +# Code above omitted πŸ‘† + +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:70-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="7" +# Code above omitted πŸ‘† + +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:70-87]!} + +# 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="5" +# Code above omitted πŸ‘† + +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:90-113]!} + +# Code below omitted πŸ‘‡ +``` +/// details | πŸ‘€ Full file preview + +```Python +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py!} +``` + +/// diff --git a/docs_src/tutorial/relationship_attributes/aliased_relationship/__init__.py b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/__init__.py similarity index 100% rename from docs_src/tutorial/relationship_attributes/aliased_relationship/__init__.py rename to docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/__init__.py diff --git a/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py similarity index 67% rename from docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py rename to docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py index 6b3172316a..40dad9462d 100644 --- a/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py @@ -18,18 +18,19 @@ class Hero(SQLModel, table=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"} + 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={"primaryjoin": "Hero.summer_team_id == Team.id"} + sa_relationship_kwargs={"foreign_keys": "Hero.summer_team_id"} ) -sqlite_file_name = "database.db" +sqlite_file_name = ":memory:" sqlite_url = f"sqlite:///{sqlite_file_name}" +mysql_url = "mysql+pymysql://root@127.0.0.1/test" -engine = create_engine(sqlite_url, echo=True) +engine = create_engine(mysql_url, echo=True) def create_db_and_tables(): @@ -69,31 +70,51 @@ def select_heroes(): with Session(engine) as session: winter_alias = aliased(Team) - # Heros with winter team as the Preventers + # 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:", heros) - assert len(heros) == 2 - summer_alias = aliased(Team) - # Heros with Preventers as their winter team and Z-Force as their summer team + # Heros with Preventers as their winter team and Z-Force as their summer team using "has" function. 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") + .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:", heros, ) - assert len(heros) == 1 assert heros[0].name == "Deadpond" diff --git a/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py similarity index 70% rename from docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py rename to docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py index 7e08522a36..fe95e2e8b6 100644 --- a/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py310.py +++ b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py @@ -16,15 +16,15 @@ class Hero(SQLModel, table=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"} + sa_relationship_kwargs={"foreign_keys": "Hero.winter_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"} + sa_relationship_kwargs={"foreign_keys": "Hero.summer_team_id"} ) -sqlite_file_name = "database.db" +sqlite_file_name = ":memory:" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url, echo=True) @@ -67,26 +67,49 @@ def select_heroes(): with Session(engine) as session: winter_alias = aliased(Team) - # Heros with winter team as the Preventers + # 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:", heros) assert len(heros) == 2 - summer_alias = aliased(Team) - # Heros with Preventers as their winter team and Z-Force as their summer team + # using "has" function. 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") + .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:", diff --git a/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py39.py similarity index 70% rename from docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py39.py rename to docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py39.py index c71d1b898d..85d003e852 100644 --- a/docs_src/tutorial/relationship_attributes/aliased_relationship/tutorial001_py39.py +++ b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py39.py @@ -18,15 +18,15 @@ class Hero(SQLModel, table=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"} + 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={"primaryjoin": "Hero.summer_team_id == Team.id"} + sa_relationship_kwargs={"foreign_keys": "Hero.summer_team_id"} ) -sqlite_file_name = "database.db" +sqlite_file_name = ":memory:" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url, echo=True) @@ -69,26 +69,49 @@ def select_heroes(): with Session(engine) as session: winter_alias = aliased(Team) - # Heros with winter team as the Preventers + # 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:", heros) assert len(heros) == 2 - summer_alias = aliased(Team) - # Heros with Preventers as their winter team and Z-Force as their summer team + # using "has" function. 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") + .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:", diff --git a/mkdocs.yml b/mkdocs.yml index a0abef5706..0fbc7f6497 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -101,7 +101,7 @@ nav: - tutorial/relationship-attributes/read-relationships.md - tutorial/relationship-attributes/remove-relationships.md - tutorial/relationship-attributes/back-populates.md - - tutorial/relationship-attributes/aliased-relationships.md + - tutorial/relationship-attributes/multiple-relationships-same-model.md - tutorial/relationship-attributes/cascade-delete-relationships.md - tutorial/relationship-attributes/type-annotation-strings.md - Many to Many: From bf737bf8c34645b0946b4df1095d3847b806d59d Mon Sep 17 00:00:00 2001 From: Cycle Driver <66476455+cycledriver@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:16:14 -0400 Subject: [PATCH 4/9] Docs: multiple relationships to to the same model-fix examples, add retreiveal of foreign info example --- .../multiple-relationships-same-model.md | 4 ++-- .../tutorial001.py | 14 ++++++++++---- .../tutorial001_py310.py | 17 ++++++++++------- .../tutorial001_py39.py | 17 ++++++++++------- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md index 70a2a8a9a6..f61d295c9b 100644 --- a/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md +++ b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md @@ -128,7 +128,7 @@ Query Heros filtering by Team attributes by manually specifying the `join` with ```Python hl_lines="7" # Code above omitted πŸ‘† -{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:70-87]!} +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:70-89]!} # Code below omitted πŸ‘‡ ``` @@ -155,7 +155,7 @@ From a query perspecitve, this is a much simpler solution. We use the `has` fun ```Python hl_lines="5" # Code above omitted πŸ‘† -{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:90-113]!} +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:91-119]!} # Code below omitted πŸ‘‡ ``` diff --git a/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py index 40dad9462d..4ebca4102e 100644 --- a/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py @@ -28,9 +28,8 @@ class Hero(SQLModel, table=True): sqlite_file_name = ":memory:" sqlite_url = f"sqlite:///{sqlite_file_name}" -mysql_url = "mysql+pymysql://root@127.0.0.1/test" -engine = create_engine(mysql_url, echo=True) +engine = create_engine(sqlite_url, echo=True) def create_db_and_tables(): @@ -85,7 +84,11 @@ def select_heroes(): """ heros = result.all() - print("Heros with Preventers as their winter team:", heros) + 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( @@ -113,8 +116,11 @@ def select_heroes(): heros = result.all() print( "Heros with Preventers as their winter and Z-Force as their summer team:", - heros, ) + 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" diff --git a/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py index fe95e2e8b6..80d355e605 100644 --- a/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py +++ b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py @@ -81,13 +81,14 @@ def select_heroes(): WHERE team_1.name = ? """ - heros = result.all() - print("Heros with Preventers as their winter team:", heros) - assert len(heros) == 2 + 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. + # 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")) @@ -113,9 +114,11 @@ def select_heroes(): heros = result.all() print( "Heros with Preventers as their winter and Z-Force as their summer team:", - heros, ) - assert len(heros) == 1 + 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" diff --git a/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py39.py index 85d003e852..4ebca4102e 100644 --- a/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py39.py +++ b/docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py39.py @@ -83,13 +83,14 @@ def select_heroes(): WHERE team_1.name = ? """ - heros = result.all() - print("Heros with Preventers as their winter team:", heros) - assert len(heros) == 2 + 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. + # 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")) @@ -115,9 +116,11 @@ def select_heroes(): heros = result.all() print( "Heros with Preventers as their winter and Z-Force as their summer team:", - heros, ) - assert len(heros) == 1 + 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" From f419d742c2630b1b4f08ad91100f6abc8be3b709 Mon Sep 17 00:00:00 2001 From: Cycle Driver <66476455+cycledriver@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:29:59 -0400 Subject: [PATCH 5/9] Tests: fix tests after refactor --- .../test_tutorial001.py | 80 ------------------- .../__init__.py | 0 .../test_tutorial001.py | 51 ++++++++++++ .../test_tutorial001_py310.py | 20 ++--- .../test_tutorial001_py39.py | 20 ++--- 5 files changed, 63 insertions(+), 108 deletions(-) delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001.py rename tests/test_tutorial/test_relationship_attributes/{test_aliased_relationship => test_multiple_relationships_same_model}/__init__.py (100%) create mode 100644 tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001.py rename tests/test_tutorial/test_relationship_attributes/{test_aliased_relationship => test_multiple_relationships_same_model}/test_tutorial001_py310.py (77%) rename tests/test_tutorial/test_relationship_attributes/{test_aliased_relationship => test_multiple_relationships_same_model}/test_tutorial001_py39.py (77%) diff --git a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001.py deleted file mode 100644 index f62a65187a..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001.py +++ /dev/null @@ -1,80 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "summer_team_id": 2, - "winter_team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "summer_team_id": 1, - "winter_team_id": 1, - }, - ], - [ - "Heros with Preventers as their winter team:", - [ - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "summer_team_id": 2, - "winter_team_id": 1, - }, - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "summer_team_id": 1, - "winter_team_id": 1, - }, - ], - ], - [ - "Heros with Preventers as their winter and Z-Force as their summer team:", - [ - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "summer_team_id": 2, - "winter_team_id": 1, - } - ], - ], -] - - -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.aliased_relationship import ( - tutorial001 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 diff --git a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/__init__.py b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/__init__.py similarity index 100% rename from tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/__init__.py rename to tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/__init__.py diff --git a/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001.py new file mode 100644 index 0000000000..843aeb251e --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001.py @@ -0,0 +1,51 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "summer_team_id": 2, + "winter_team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "summer_team_id": 1, + "winter_team_id": 1, + }, + ], + ["Heros with Preventers as their winter team:"], + ["Hero: Deadpond, Winter Team: Preventers Summer Team: Z-Force"], + ["Hero: Rusty-Man, Winter Team: Preventers Summer Team: Preventers"], + ["Heros with Preventers as their winter and Z-Force as their summer team:"], + ["Hero: Deadpond, Winter Team: Preventers Summer Team: Z-Force"], +] + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.multiple_relationships_same_model import ( + tutorial001 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 diff --git a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py310.py similarity index 77% rename from tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py310.py rename to tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py310.py index 002512afa9..7d4ccc598f 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py310.py @@ -48,25 +48,17 @@ }, ], ], - [ - "Heros with Preventers as their winter and Z-Force as their summer team:", - [ - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "summer_team_id": 2, - "winter_team_id": 1, - } - ], - ], + ["Heros with Preventers as their winter team:"], + ["Hero: Deadpond, Winter Team: Preventers Summer Team: Z-Force"], + ["Hero: Rusty-Man, Winter Team: Preventers Summer Team: Preventers"], + ["Heros with Preventers as their winter and Z-Force as their summer team:"], + ["Hero: Deadpond, Winter Team: Preventers Summer Team: Z-Force"], ] @needs_py310 def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.aliased_relationship import ( + from docs_src.tutorial.relationship_attributes.multiple_relationships_same_model import ( tutorial001_py310 as mod, ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py39.py similarity index 77% rename from tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py39.py rename to tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py39.py index 2acf294ee6..a4a7614327 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_aliased_relationship/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py39.py @@ -48,25 +48,17 @@ }, ], ], - [ - "Heros with Preventers as their winter and Z-Force as their summer team:", - [ - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "summer_team_id": 2, - "winter_team_id": 1, - } - ], - ], + ["Heros with Preventers as their winter team:"], + ["Hero: Deadpond, Winter Team: Preventers Summer Team: Z-Force"], + ["Hero: Rusty-Man, Winter Team: Preventers Summer Team: Preventers"], + ["Heros with Preventers as their winter and Z-Force as their summer team:"], + ["Hero: Deadpond, Winter Team: Preventers Summer Team: Z-Force"], ] @needs_py39 def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.aliased_relationship import ( + from docs_src.tutorial.relationship_attributes.multiple_relationships_same_model import ( tutorial001_py39 as mod, ) From dfdaf2385457ad4bbd0561b807ccf60c1ea7286c Mon Sep 17 00:00:00 2001 From: Cycle Driver <66476455+cycledriver@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:34:34 -0400 Subject: [PATCH 6/9] Tests: fix tests after refactor --- .../test_tutorial001_py310.py | 21 ------------------- .../test_tutorial001_py39.py | 21 ------------------- 2 files changed, 42 deletions(-) diff --git a/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py310.py index 7d4ccc598f..7d10c8cf73 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py310.py @@ -27,27 +27,6 @@ "winter_team_id": 1, }, ], - [ - "Heros with Preventers as their winter team:", - [ - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "summer_team_id": 2, - "winter_team_id": 1, - }, - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "summer_team_id": 1, - "winter_team_id": 1, - }, - ], - ], ["Heros with Preventers as their winter team:"], ["Hero: Deadpond, Winter Team: Preventers Summer Team: Z-Force"], ["Hero: Rusty-Man, Winter Team: Preventers Summer Team: Preventers"], diff --git a/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py39.py index a4a7614327..bdfd97f0d9 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_relationship_attributes/test_multiple_relationships_same_model/test_tutorial001_py39.py @@ -27,27 +27,6 @@ "winter_team_id": 1, }, ], - [ - "Heros with Preventers as their winter team:", - [ - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "summer_team_id": 2, - "winter_team_id": 1, - }, - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "summer_team_id": 1, - "winter_team_id": 1, - }, - ], - ], ["Heros with Preventers as their winter team:"], ["Hero: Deadpond, Winter Team: Preventers Summer Team: Z-Force"], ["Hero: Rusty-Man, Winter Team: Preventers Summer Team: Preventers"], From d514df2c1e2198b3639c6c5f9daca210e1391310 Mon Sep 17 00:00:00 2001 From: Cycle Driver <66476455+cycledriver@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:43:17 -0400 Subject: [PATCH 7/9] Doocs: fix highlighting --- .../multiple-relationships-same-model.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md index f61d295c9b..d67ec0b468 100644 --- a/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md +++ b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md @@ -69,7 +69,7 @@ If the foreign table search criteria (address.zipcode) is not unique, prefer EXI 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. -{* ./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py ln[13:26] hl[11,15] *} +{* ./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py ln[13:26] hl[9,13] *} 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. @@ -116,7 +116,7 @@ and a couple of extra bits of info in our **SQLModel** join statements. ```Python hl_lines="2" # Code above omitted πŸ‘† -{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:70-71]!} +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:69-71]!} # Code below omitted πŸ‘‡ ``` @@ -128,7 +128,7 @@ Query Heros filtering by Team attributes by manually specifying the `join` with ```Python hl_lines="7" # Code above omitted πŸ‘† -{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:70-89]!} +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:69-89]!} # Code below omitted πŸ‘‡ ``` @@ -152,10 +152,10 @@ For more information see [SQLAlchemy: Handling Multiple Join Paths](https://docs From a query perspecitve, this is a much simpler solution. We use the `has` function in the query: -```Python hl_lines="5" +```Python hl_lines="4 5" # Code above omitted πŸ‘† -{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:91-119]!} +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:93-123]!} # Code below omitted πŸ‘‡ ``` From 1f80fcb3e7aa05c1e8e8602d070ae0440952ad37 Mon Sep 17 00:00:00 2001 From: Cycle Driver <66476455+cycledriver@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:50:40 -0400 Subject: [PATCH 8/9] Doocs: fix highlighting --- .../multiple-relationships-same-model.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md index d67ec0b468..64a888bbc0 100644 --- a/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md +++ b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md @@ -69,7 +69,13 @@ If the foreign table search criteria (address.zipcode) is not unique, prefer EXI 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. -{* ./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001_py310.py ln[13:26] hl[9,13] *} +```Python hl_lines="9 13" +# 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. @@ -113,7 +119,7 @@ The aliases we create are `home_address_alias` and `work_address_alias`. You ca 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="2" +```Python hl_lines="4" # Code above omitted πŸ‘† {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:69-71]!} @@ -125,7 +131,7 @@ and a couple of extra bits of info in our **SQLModel** join statements. 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="7" +```Python hl_lines="9" # Code above omitted πŸ‘† {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:69-89]!} @@ -148,11 +154,11 @@ For more information see [SQLAlchemy: Handling Multiple Join Paths](https://docs /// -#### Correlated Sub Query +### Correlated Sub Query From a query perspecitve, this is a much simpler solution. We use the `has` function in the query: -```Python hl_lines="4 5" +```Python hl_lines="6 7" # Code above omitted πŸ‘† {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:93-123]!} From a6e14924f29fa29834344c4aab82a35ce7301498 Mon Sep 17 00:00:00 2001 From: Cycle Driver <66476455+cycledriver@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:55:53 -0400 Subject: [PATCH 9/9] Doocs: fix highlighting --- .../multiple-relationships-same-model.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md index 64a888bbc0..4969f18150 100644 --- a/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md +++ b/docs/tutorial/relationship-attributes/multiple-relationships-same-model.md @@ -13,7 +13,7 @@ Query: Find users with home address zipcode "100000" and work address zipcode = ### Alias Query Using an alias, JOIN the address table twice: -``` +```sql SELECT * FROM user JOIN address AS home_address_alias @@ -27,7 +27,7 @@ WHERE ### Correlated Sub-Query Using sub-queries, filter the matches with EXISTS: -``` +```sql SELECT * FROM user WHERE ( @@ -69,7 +69,7 @@ If the foreign table search criteria (address.zipcode) is not unique, prefer EXI 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="9 13" +```Python hl_lines="11 15" # Code above omitted πŸ‘† {!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:13-26]!} @@ -134,7 +134,7 @@ Query Heros filtering by Team attributes by manually specifying the `join` with ```Python hl_lines="9" # Code above omitted πŸ‘† -{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:69-89]!} +{!./docs_src/tutorial/relationship_attributes/multiple_relationships_same_model/tutorial001.py[ln:69-91]!} # Code below omitted πŸ‘‡ ```