sqlmodel icon indicating copy to clipboard operation
sqlmodel copied to clipboard

from __future__ import annotation and many to many example

Open 5cat opened this issue 3 years ago • 16 comments

First Check

  • [X] I added a very descriptive title to this issue.
  • [X] I used the GitHub search to find a similar issue and didn't find it.
  • [X] I searched the SQLModel documentation, with the integrated search.
  • [X] I already searched in Google "How to X in SQLModel" and didn't find any information.
  • [X] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [X] I already checked if it is not related to SQLModel but to Pydantic.
  • [X] I already checked if it is not related to SQLModel but to SQLAlchemy.

Commit to Help

  • [X] I commit to help with one of those options 👆

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

5cat avatar Dec 18 '21 04:12 5cat

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.

Batalex avatar Jan 13 '22 09:01 Batalex

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...

l1b3r avatar Jan 27 '22 22:01 l1b3r

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

xavipolo avatar Sep 11 '22 09:09 xavipolo

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

agronholm avatar Mar 10 '23 18:03 agronholm

I experienced the same issue on version sqlmodel == 0.0.8

NunchakusLei avatar Sep 27 '23 08:09 NunchakusLei

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

JamesHutchison avatar Nov 21 '23 02:11 JamesHutchison

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

s-weigand avatar Dec 06 '23 21:12 s-weigand

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.

aholten avatar Jan 08 '24 17:01 aholten

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?

pawrequest avatar Jan 10 '24 02:01 pawrequest

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]...

DirkReiners avatar Feb 01 '24 15:02 DirkReiners

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"]

NunchakusLei avatar Feb 01 '24 18:02 NunchakusLei

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

JamesHutchison avatar Feb 01 '24 18:02 JamesHutchison

Seems reasonable to me that it would fix the problem.

agronholm avatar Feb 01 '24 22:02 agronholm