fastapi / sqlmodel

SQL databases in Python, designed for simplicity, compatibility, and robustness.
https://sqlmodel.tiangolo.com/
MIT License
14.11k stars 624 forks source link

from __future__ import annotation and many to many example #196

Open 5cat opened 2 years ago

5cat commented 2 years ago

First Check

Commit to Help

Example Code

from __future__ import annotations

from typing import List, Optional

from sqlmodel import Field, Relationship, Session, SQLModel, create_engine

class HeroTeamLink(SQLModel, table=True):
    team_id: Optional[int] = Field(
        default=None, foreign_key="team.id", primary_key=True
    )
    hero_id: Optional[int] = Field(
        default=None, foreign_key="hero.id", primary_key=True
    )

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

    heroes: List[Hero] = Relationship(back_populates="teams", link_model=HeroTeamLink)

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

    teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink)

sqlite_url = f"sqlite://"

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",
            teams=[team_z_force, team_preventers],
        )
        hero_rusty_man = Hero(
            name="Rusty-Man",
            secret_name="Tommy Sharp",
            age=48,
            teams=[team_preventers],
        )
        hero_spider_boy = Hero(
            name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers]
        )
        session.add(hero_deadpond)
        session.add(hero_rusty_man)
        session.add(hero_spider_boy)
        session.commit()

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

        print("Deadpond:", hero_deadpond)
        print("Deadpond teams:", hero_deadpond.teams)
        print("Rusty-Man:", hero_rusty_man)
        print("Rusty-Man Teams:", hero_rusty_man.teams)
        print("Spider-Boy:", hero_spider_boy)
        print("Spider-Boy Teams:", hero_spider_boy.teams)

def main():
    create_db_and_tables()
    create_heroes()

if __name__ == "__main__":
    main()

Description

all what i have done is use the example from the docs about many to many relationships and tried to fit them with from __future__ import annotation by adding the import in the first line and changing List["Hero"] to List[Hero]. and i get this error

sqlalchemy.exc.InvalidRequestError: When initializing mapper mapped class Team->team, expression 'List[Hero]' failed to locate a name ('List[Hero]'). If this is a class name, consider adding this relationship() to the <class '__main__.Team'> class after both dependent classes have been defined.

also update_forward_refs() did not help for both classes.

Operating System

Linux

Operating System Details

No response

SQLModel Version

0.0.4

Python Version

Python 3.9.9

Additional Context

No response

Batalex commented 2 years ago

Hi, I have the same issue with sqlmodel 0.0.6 and python 3.9.9.

I have created a similar virtual env with python 3.8.10 and this piece of code works as expected.

l1b3r commented 2 years ago

I had a similar issue with 1-M relationship, where the models are also separated into their own files.

sqlmodel == 0.0.6
python == 3.9

Not sure if that helps, but I was able to workaround this issue by removing

from __future__ import annotations

from the file of the "parent" model and returning back to quoting the type hints:

# other imports omitted

from typing import TYPE_CHECKING:
    from .child import ChildModel

class ParentModel(SQLModel, table=True):
    #    children: List[ChildModel] = Relationship(back_populates="parent")  # does not work
    children: List["ChildModel"] = Relationship(back_populates="parent")  # OK

    #  other column definitions...
xavipolo commented 2 years ago

I had a similar issue with 1-M relationship, where the models are also separated into their own files.

sqlmodel == 0.0.6
python == 3.9

@l1b3r Do you updated to lastest versions?

I am having some problems with the models separated in different files. The example model of Heroes and Teams, works fine with separate files until I add the model that allows to get a Team with its Heroes. https://sqlmodel.tiangolo.com/tutorial/fastapi/relationships/#update-the-path-operations

Are you using this option, does it work for you?

Thanks

agronholm commented 1 year ago

It looks like the forward reference is not evaluated at all, but passed to SQLAlchemy as-is.

NunchakusLei commented 11 months ago

I experienced the same issue on version sqlmodel == 0.0.8

JamesHutchison commented 10 months ago

This is a pretty confusing problem to have given the selling point of this library.

s-weigand commented 9 months ago

Postponed annotations are still an issue with 0.0.14, hence the ruff setting. I guess it has to do with all the very dark black magic that SQLAlchemy does 😅

I also hit this issue (again) with a new project, But removing

from __future__ import annotations

fixed it (again). Just in case someone else hits this issue

aholten commented 8 months ago

Thank you @s-weigand, I removed the annotations import and SQLAlchemy stopped complaining that classes weren't mapped!

Would have saved me time if I was told that future annotations cannot be used, and that forward type references in SQLModel classes should be enclosed in double quotes to play nice with SA. I guess I should have gleaned this from the docs, on the page about type annotation strings? Considering this issue thread though, I think it's worth addressing explicitly somewhere. I'll look into adding this caveat to the docs if contributions are accepted.

pawrequest commented 8 months ago

yeah i lost loads of time to this, definitely think 'don't import annotations from future' should be stated somewhere in the docs. unless i missed it?

DirkReiners commented 7 months ago

While documentation is important, is there a plan or a route to fix this? Strings don't work for nice things like Optional[A | B | C]...

NunchakusLei commented 7 months ago

While documentation is important, is there a plan or a route to fix this? Strings don't work for nice things like Optional[A | B | C]...

I think you can do something like this Optional["A" | "B" | "C"]

aholten commented 7 months ago

@DirkReiners Forward referenced type annotations are a planned future feature for Python, so I wouldn't expect this library to implement a fix.

I explain the situation and state of the feature in a bit more detail in my PR

JamesHutchison commented 7 months ago

Just to double check - this isn't something that would be fixed by adding typing.get_type_hints(...) somewhere, would it?

agronholm commented 7 months ago

Seems reasonable to me that it would fix the problem.

jbaudisch commented 1 month ago

Having the same issue.

I had a similar issue with 1-M relationship, where the models are also separated into their own files.

sqlmodel == 0.0.6
python == 3.9

Not sure if that helps, but I was able to workaround this issue by removing

from __future__ import annotations

from the file of the "parent" model and returning back to quoting the type hints:

# other imports omitted

from typing import TYPE_CHECKING:
    from .child import ChildModel

class ParentModel(SQLModel, table=True):
    #    children: List[ChildModel] = Relationship(back_populates="parent")  # does not work
    children: List["ChildModel"] = Relationship(back_populates="parent")  # OK

    #  other column definitions...

Although this workaround works for the time being, the code will no longer work in the future (when __future__.annotations become the default), so this should be fixed.