tiangolo / sqlmodel

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

Class .... is not mapped with polymorphic identity #438

Open zopyx opened 1 year ago

zopyx commented 1 year ago

First Check

Commit to Help

Example Code

from typing import Optional
import uuid
from typing import List

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

class InvoiceRequest(SQLModel, table=True):

    __tablename__ = "invoice_requests"

    id: UUID = Field(default_factory=uuid.uuid4, primary_key=True)
    product: str | None = None
    request_type: str | None = None

    invoices : List["Invoice"] = Relationship(back_populates="invoice_request")

    def add_invoices(self):
        self.invoices.append(InvoiceReversal())
        self.invoices.append(Invoice())

class Invoice(SQLModel, table=True):

    __tablename__ = "invoices"

    id: UUID = Field(default_factory=uuid.uuid4, primary_key=True)
    invoice_type: str = Field(default="regular")

    invoice_request_id: UUID | None = Field(default=None, foreign_key="invoice_requests.id")
    invoice_request: InvoiceRequest = Relationship(back_populates="invoices")

    __mapper_args__ = {
        "polymorphic_on": 'invoice_type',
        "polymorphic_identity": "regular",
    }

class InvoiceReversal(Invoice, table=True):

    __mapper_args__ = {
        "polymorphic_identity": "reversal",
    }

class InvoiceCorrection(Invoice, table=True):

    __mapper_args__ = {
        "polymorphic_identity": "correction",
    }

db_fn = "db.sqlite"
db_url = f"sqlite:///{db_fn}"
engine = create_engine(db_url, echo=True)
SQLModel.metadata.create_all(engine)

with Session(engine) as session:
    ivr = InvoiceRequest(product="2", request_type="abc")
    ivr.add_invoices()
    session.add(ivr)
    session.commit()

Description

I have this PoC-style code that would model a 1:n relationship between one InvoiceRequest and multiple Invoices.

There are several invoice types ÃŒnvoiceReversal and InvoiceCorrection that are modelled using the inheritance from Invoice class and polymorphic identity.

However. the derived classes are obviously not mapped:

sqlalchemy.orm.exc.UnmappedClassError: Class '__main__.InvoiceReversal' is not mapped

I could not find a solution how to resolve this.

Operating System

Linux

Operating System Details

Linux

SQLModel Version

SQLAlchemy==1.4.40

Python Version

3.10

Additional Context

python3 foo2.py
2022-09-06 11:55:17,985 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2022-09-06 11:55:17,985 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("invoice_requests")
2022-09-06 11:55:17,985 INFO sqlalchemy.engine.Engine [raw sql] ()
2022-09-06 11:55:17,985 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("invoices")
2022-09-06 11:55:17,985 INFO sqlalchemy.engine.Engine [raw sql] ()
2022-09-06 11:55:17,986 INFO sqlalchemy.engine.Engine COMMIT
Traceback (most recent call last):
  File "/home/ajung/src/sonnen/sonnen-maia-proof-of-concept/sonnen/maia/planner/foo2.py", line 63, in <module>
    session.add(ivr)
  File "/home/ajung/src/sonnen/sonnen-maia-proof-of-concept/.venv/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2626, in add
    self._save_or_update_state(state)
  File "/home/ajung/src/sonnen/sonnen-maia-proof-of-concept/.venv/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2642, in _save_or_update_state
    for o, m, st_, dct_ in mapper.cascade_iterator(
  File "/home/ajung/src/sonnen/sonnen-maia-proof-of-concept/.venv/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 3230, in cascade_iterator
    queue = deque(
  File "/home/ajung/src/sonnen/sonnen-maia-proof-of-concept/.venv/lib/python3.10/site-packages/sqlalchemy/orm/relationships.py", line 2020, in cascade_iterator
    instance_mapper = instance_state.manager.mapper
  File "/home/ajung/src/sonnen/sonnen-maia-proof-of-concept/.venv/lib/python3.10/site-packages/sqlalchemy/util/langhelpers.py", line 1113, in __get__
    obj.__dict__[self.__name__] = result = self.fget(obj)
  File "/home/ajung/src/sonnen/sonnen-maia-proof-of-concept/.venv/lib/python3.10/site-packages/sqlalchemy/orm/instrumentation.py", line 204, in mapper
    raise exc.UnmappedClassError(self.class_)
daniil-berg commented 1 year ago

I can confirm. Here is a simplified version for testing (compatible with Python 3.7+):

from typing import List, Optional

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

class Foo(SQLModel, table=True):
    id: Optional[int] = Field(primary_key=True)

    bars: List["Bar"] = Relationship(back_populates="foo")

class Bar(SQLModel, table=True):
    __mapper_args__ = {
        "polymorphic_on": "type",
        "polymorphic_identity": "x",
    }

    id: Optional[int] = Field(primary_key=True)
    type: str = Field(default="x")

    foo_id: Optional[int] = Field(foreign_key="foo.id")
    foo: Optional[Foo] = Relationship(back_populates="bars")

class BarY(Bar, table=True):
    __mapper_args__ = {
        "polymorphic_identity": "y",
    }

class BarZ(Bar, table=True):
    __mapper_args__ = {
        "polymorphic_identity": "z",
    }

db_url = f"sqlite:///:memory:"
engine = create_engine(db_url, echo=True)
SQLModel.metadata.create_all(engine)

with Session(engine) as session:
    foo = Foo()
    foo.bars.extend([BarY(), Bar()])
    session.add(foo)
    session.commit()
The full error: (Click to expand) ``` Traceback (most recent call last): File "/home/daniil/coding/sqlmodel/polymorphic.py", line 45, in session.add(foo) File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2626, in add self._save_or_update_state(state) File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2642, in _save_or_update_state for o, m, st_, dct_ in mapper.cascade_iterator( File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/orm/mapper.py", line 3230, in cascade_iterator queue = deque( File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/orm/relationships.py", line 2020, in cascade_iterator instance_mapper = instance_state.manager.mapper File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/util/langhelpers.py", line 1113, in __get__ obj.__dict__[self.__name__] = result = self.fget(obj) File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/orm/instrumentation.py", line 204, in mapper raise exc.UnmappedClassError(self.class_) sqlalchemy.orm.exc.UnmappedClassError: Class '__main__.BarY' is not mapped ```
Here is an analogous version in pure SQLAlchemy working with no errors: (Click to expand) ```python from sqlalchemy.engine.create import create_engine from sqlalchemy.orm import relationship from sqlalchemy.orm.decl_api import declarative_base from sqlalchemy.sql.schema import Column, ForeignKey from sqlalchemy.orm.session import Session from sqlalchemy.sql.sqltypes import Integer, String Base = declarative_base() class Foo(Base): __tablename__ = 'foo' id = Column(Integer, primary_key=True) bars = relationship("Bar", back_populates="foo") class Bar(Base): __tablename__ = 'bar' __mapper_args__ = { "polymorphic_on": "type", "polymorphic_identity": "x", } id = Column(Integer, primary_key=True) type = Column(String, default="x") foo_id = Column(ForeignKey("foo.id")) foo = relationship("Foo", back_populates="bars") class BarY(Bar): __mapper_args__ = { "polymorphic_identity": "y", } class BarZ(Bar): __mapper_args__ = { "polymorphic_identity": "z", } db_url = f"sqlite:///:memory:" engine = create_engine(db_url, echo=True) Base.metadata.create_all(engine) with Session(engine) as session: foo = Foo() foo.bars.extend([BarY(), Bar()]) session.add(foo) session.commit() ```

Interestingly, you get a different error, if you instead just try to instantiate a BarY object:

...
with Session(engine) as session:
    bar = BarY()
    session.add(bar)
    session.commit()
The full error: (Click to expand) ``` Traceback (most recent call last): File "/home/daniil/coding/sqlmodel/polymorphic.py", line 46, in bar = BarY() File "", line 4, in __init__ File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/orm/state.py", line 481, in _initialize_instance with util.safe_reraise(): File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/util/langhelpers.py", line 70, in __exit__ compat.raise_( File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/util/compat.py", line 208, in raise_ raise exception File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/orm/state.py", line 479, in _initialize_instance return manager.original_init(*mixed[1:], **kwargs) File "", line 6, in __init__ File "/home/daniil/coding/sqlmodel/sqlmodel/main.py", line 518, in __init__ setattr(__pydantic_self__, key, value) File "/home/daniil/coding/sqlmodel/sqlmodel/main.py", line 532, in __setattr__ set_attribute(self, name, value) File "/home/daniil/.cache/pypoetry/virtualenvs/sqlmodel-TV15XYpK-py3.10/lib/python3.10/site-packages/sqlalchemy/orm/attributes.py", line 2256, in set_attribute state.manager[key].impl.set(state, dict_, value, initiator) AttributeError: 'NoneType' object has no attribute 'set' ```

I tried debugging the SQLModelMetaclass.__new__ method, to see if the __mapper_args__ end up on the resulting class, and it seems like they are. I checked this line here:

new_cls = super().__new__(cls, name, bases, dict_used, **config_kwargs)

When setting up Bar for example, the new_cls.__dict__ contains the key value pair:

__mapper_args__: {'polymorphic_on': 'type', 'polymorphic_identity': 'x'}

So that seems not to be the issue.

This is all I got so far.

zopyx commented 1 year ago

Someone helped me out with a slightly modified version that works for me:

from typing import Optional
import uuid
from typing import List

from sqlmodel import Field, SQLModel, create_engine, Session, Relationship
from uuid import UUID
from sqlalchemy.orm import registry
mapper_registry = registry()

class InvoiceRequest(SQLModel, table=True):

    __tablename__ = "invoice_requests"

    id: UUID = Field(default_factory=uuid.uuid4, primary_key=True)
    product: str | None = None
    request_type: str | None = None

    invoices : List["Invoice"] = Relationship(back_populates="invoice_request")

    def add_invoices(self):
        self.invoices.append(InvoiceReversal())
        self.invoices.append(InvoiceCorrection())

class Invoice(SQLModel, table=True):

#    __tablename__ = "invoices"

    id: UUID = Field(default_factory=uuid.uuid4, primary_key=True)
    invoice_type: str = Field(default="regular")

    invoice_request_id: UUID | None = Field(default=None, foreign_key="invoice_requests.id")
    invoice_request: InvoiceRequest = Relationship(back_populates="invoices")

    __mapper_args__ = {
        "polymorphic_on": 'invoice_type',
        "polymorphic_identity": "regular",
    }

@mapper_registry.mapped
class InvoiceReversal(Invoice, table=True):
    invoice_types: str = Field(default="regular")
    __mapper_args__ = {
        "polymorphic_identity": "reversal",
        "inherit_condition": invoice_types == Invoice.invoice_type
    }

@mapper_registry.mapped
class InvoiceCorrection(Invoice, table=True):
    invoice_types: str = Field(default="correction")
    __mapper_args__ = {
        "polymorphic_identity": "correction",
        "inherit_condition": invoice_types == Invoice.invoice_type
    }
mxdev88 commented 1 year ago

I tried this workaround with @mapper_registry.mapped but I get "sqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedColumn) column xxx does not exist".

With "inherit_condition" SQLAlchemy maked the correct join but thinks all columns are in the child table. There also a warning on start up, "SAWarning: Implicitly combining column parent.id with column child.id under attribute 'id'. Please configure one or more attributes for these same-named columns explicitly."

Any idea how to make this work?

KunxiSun commented 3 weeks ago

Hello~, the current version is sqlmodel 0.0.19. Does anyone have idea about how to solve this? @mapper_registry.mapped not work for me