sqlmodel icon indicating copy to clipboard operation
sqlmodel copied to clipboard

Class .... is not mapped with polymorphic identity

Open zopyx opened this issue 2 years ago • 4 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 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_)

zopyx avatar Sep 06 '22 10:09 zopyx

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 <module>
    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)
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 <module>
    bar = BarY()
  File "<string>", 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 "<string>", 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.

daniil-berg avatar Sep 06 '22 14:09 daniil-berg

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
    }

zopyx avatar Sep 06 '22 14:09 zopyx

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?

mxdev88 avatar Jul 28 '23 21:07 mxdev88

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

KunxiSun avatar Jul 03 '24 05:07 KunxiSun