sqlmodel
sqlmodel copied to clipboard
Class .... is not mapped with polymorphic identity
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_)
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.
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
}
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?
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