fastapi / sqlmodel

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

Unable to use original type hint by defining relationship #228

Open Whitroom opened 2 years ago

Whitroom commented 2 years ago

First Check

Commit to Help

Example Code

from typing import Optional

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

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

    # key sentence
    heroes: list["Hero"] = Relationship(back_populates='team')

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)

    team_id: Optional[int] = Field(default=None, foreign_key=Team.id)

database_url = f'mysql+mysqlconnector://{user}:{password}@{host}/{database_name}'

engine = create_engine(database_url, encoding='utf8', echo=True)

if __name__ == '__main__':
    SQLModel.metadata.create_all(engine)

Description

I create Hero and Team models and try to use python original type hint, but while running it, it occurs exceptions...

Traceback (most recent call last): File "pydantic\validators.py", line 709, in pydantic.validators.find_validators TypeError: issubclass() arg 1 must be a class

Traceback (most recent call last): File "C:\Users\10620\Desktop\Code\Python\sqlmodel\table_relationship.py", line 5, in
class Team(SQLModel, table=True): File "C:\Users\10620\AppData\Local\Programs\Python\Python310\lib\site-packages\sqlmodel\main.py", line 342, in init temp_field = ModelField.infer( File "pydantic\fields.py", line 488, in pydantic.fields.ModelField.infer File "pydantic\fields.py", line 419, in pydantic.fields.ModelField.init File "pydantic\fields.py", line 534, in pydantic.fields.ModelField.prepare File "pydantic\fields.py", line 728, in pydantic.fields.ModelField._type_analysis File "pydantic\fields.py", line 778, in pydantic.fields.ModelField._create_sub_type File "pydantic\fields.py", line 419, in pydantic.fields.ModelField.init File "pydantic\fields.py", line 539, in pydantic.fields.ModelField.prepare File "pydantic\fields.py", line 801, in pydantic.fields.ModelField.populate_validators File "pydantic\validators.py", line 718, in find_validators RuntimeError: error checking inheritance of 'Hero' (type: str)

The solution is adding List from typing and replacing it, but I don't know how to solve this bug...

link: https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/define-relationships-attributes/

Operating System

Windows

Operating System Details

No response

SQLModel Version

0.0.6

Python Version

3.10.1

Additional Context

No response

byrman commented 2 years ago

I am not sure I understand your question correctly, but here list["Hero"] should indeed be List["Hero"]. From Python 3.9 onwards, generic list[Hero] should work as well, I think, but that requires your code to be rearranged, so that Hero is defined before Team.

aleksul commented 2 years ago

@Whitroom pls update to pydantic v1.9.1 - it should be fixed

Shuntw6096 commented 9 months ago

I meet a similar problem,

class Base(SQLModel):
    pass
class Test:
    basemodel = Base # type alias at here, classVar[type[Base]] also didn't work
    __slots__ = 'filename', 'datamodel'
    def __init__(self, filename: str, datamodel: Optional[Base] = None, **kwargs):
        self.filename = filename
        if datamodel is not None:
            assert isinstance(datamodel, self.basemodel) # self.basemodel is Any at here, should be type[Base]
            self.datamodel = datamodel
        else:
            self.datamodel = self.basemodel(**kwargs) # self.basemodel is Any at here, should be type[Base]

If you remove inheritance of Base, typehint work perfectly. My stupid solution is adding a classmethod to get instance of Base,

class Base(SQLModel, table=True):
    @classmethod
    def get_instance(cls, data: dict):
        return Base(**data)

class Test:
    basemodel = Base.get_instance
    __slots__ = 'filename', 'datamodel'
    def __init__(self, filename: str, datamodel: Optional[Base] = None, **kwargs):
        self.filename = filename
        if datamodel is not None:
            assert isinstance(datamodel, self.basemodel)
            self.datamodel = datamodel
        else:
            self.datamodel = self.basemodel(**kwargs)

Btw, filename is private attribute of Base at first, but idk how to init it.

thribhu commented 2 months ago

Many to one relationship using sqlmodel and sqlalchemy along with alembic

When initializing mapper Mapper[Category(category)], expression \"relationship(\"Mapped[List['SubCategory']]\")\" seems to be using a generic class as the argument to relationship(); please state the generic argument using an annotation, e.g. \"subcategories: Mapped[Mapped[List['SubCategory']']] = relationship()\

I am facing an issue with many to one relationship. Having followed the official documentation, I created my relationship attributes inside my table. However, I receive an error from sqlalchemy like the above. My code is simple

from sqlmodel import SQLModel
from sqlmodel import Field
from sqlmodel import Relationship
from typing import Optional
from typing import List

class Category(SQLModel, table=True):
  id: int | None = Field(default=None, primary_key=True)
  name: str = Field(..., title="Category name")
  subcategories: List['SubCategory'] = Relationship(back_populates='category')

class SubCategory(SQLModel, table=True):
  id: int | None = Field(default=None, primary_key=True)
  name: str = Field(..., title="Sub category name")
  category_id: int | None = Field(default=None, foreign_key='category.id')
  category: 'Category' | None = Relationship(back_populates='subcategories')
nicksonthc commented 2 weeks ago

Some updates here: I tested the tutorial code with the sample provided and found that using from future import annotations might cause a mapper error. After removing the import, it worked.

class Team(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    headquarters: str

    heroes: list["Hero"] = Relationship(back_populates="team")

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)

    team_id: int | None = Field(default=None, foreign_key="team.id")
    team: Team | None = Relationship(back_populates="heroes")

Import annotations might cause the mapper error

If in my model file got import annotations, then i will get the mappers error, after i remove from __future__ import annotations from __future__ import annotations

Error image

Tutorial i refer to - https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/define-relationships-attributes/#declare-relationship-attributes

Context & Environments

python 3.12 pydantic==2.9.2 SQLAlchemy==2.0.35 sqlmodel==0.0.22