Handling optional attributes in SQLAlchemy Declarative Mapping abstract classes with dataclass #10027
-
DescriptionI'm currently trying to implement SQLAlchemy's Declarative Mapping with Pydantic's dataclass with SQLAlchemy 2.0 in an Async context. However, I encountered a use case that I'm unable to solve with my existing ObjectiveThe workflow that I'm trying to achieve is as follows:
The challenge lies in handling the optional attribute ProblemThe current implementation does not handle
However, what I expect to see is: QuestionIs there an alternative approach to Environment and SetupThe issue can be reproduced in the following environment:
The relevant model and API are as follows: My model: class Base(
MappedAsDataclass,
DeclarativeBase,
dataclass_callable=pydantic.dataclasses.dataclass,
):
class Config:
orm_mode = True
class BInput(Base):
__abstract__ = True
name: Mapped[str] = mapped_column(String(50))
attribute1: Mapped[bool] = mapped_column(Boolean(), default=False)
class BOutput(BInput):
__tablename__ = "b"
attribute2: Mapped[int] = mapped_column(Integer(), default=100)
computed_info_1: int = 0
computed_info_2: str = "" My API: @router.post("", response_model=BOutput, status_code=201)
async def add(*, db: AsyncSession = Depends(get_db), b_input: BInput):
b_output = BOutput(**asdict(b_input))
b_output.computed_info_2 = "Some custom logic too add here"
db.add(b_output)
await db.commit()
return b_output Test CaseI've also provided a test case to help reproduce the issue: @pytest.mark.asyncio
async def test_create_b(db_session: AsyncSession, client: AsyncClient):
response = await client.post(
"/v1/b",
json={
"name": "Test B 1",
},
)
assert response.status_code == 201
pack = response.json()
print(pack)
assert pack["name"] == "Test B 1"
assert pack["attribute1"] == False
assert pack["attribute2"] == 100 ErrorThe error that occurs during execution is as follows: Python stack traceback
If required, I can provide a minimal reproducible example which will be quite similar to the one I've provided here. Looking forward to any insights and guidance that might help me resolve this issue. Update: I've clarified and updated the original issue to ease troubleshooting |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 19 replies
-
Hi, I'm not sure what's at fault. Trying to create a runnable example from your snipped does not seem to result in any error. Try to modify it to make it return an error EDIT: updated script to reproduce. This happens also with plain dataclasses from sqlalchemy.orm import (
DeclarativeBase,
MappedAsDataclass,
Mapped,
mapped_column,
)
from sqlalchemy import Boolean, Integer, String
import dataclasses as DC
import pydantic
class Base(
MappedAsDataclass,
DeclarativeBase,
# dataclass_callable=pydantic.dataclasses.dataclass,
):
class Config:
orm_mode = True
class BInput(Base):
__abstract__ = True
id: Mapped[int] = mapped_column(Integer, primary_key=True, init=False)
name: Mapped[str] = mapped_column(String(50))
attribute1: Mapped[bool] = mapped_column(Boolean(), default=False)
class BOutput(BInput):
__tablename__ = "b"
attribute2: Mapped[int] = mapped_column(Integer(), default=100)
computed_info_1: int = 0
computed_info_2: str = ""
b_input = BInput(name="foo")
print(DC.asdict(b_input))
b_output = BOutput(**DC.asdict(b_input))
b_output.attribute2 = 42
print(DC.asdict(b_output)) |
Beta Was this translation helpful? Give feedback.
-
There's no ORM constructor or ORM-handling of these attributes on the abstract class because it's not valid to instantiate an abstract class: https://en.wikipedia.org/wiki/Abstract_type
SQLAlchemy isn't adding special steps here to prevent the instantiation but I guess we should. |
Beta Was this translation helpful? Give feedback.
-
A SQLAlchemy class is mapped to a database table, always, either directly or via inheritance. that's their purpose. if you want to instantiate classes that are not mapped to database tables, then you would not make that class a SQLAlchemy mapped class. So make a separate class that does the business case you need and don't map it (that is, does not extend from the declarative base at all). A mixin class satisfies this need as well.
right but you are putting mapped_column() directives on that class, which doesnt make sense if you want to instantiate that class and have it not be associated with any database table. pydantic has some kind of "two class approach" to this sort of thing, apparently.
a mixin or abstract class can have this, but they are not mapped, so instantiation makes no sense. instantiation means the class is managed from a persistence perspective and requires a Mapper. The Mapper does not have a "exist, but we are not really mapped to any table" mode - that would be extremely complicated without there being a real use case. the mapped_column() directives are not actual instance-level constructs, when the class is mapped, these get replaced with Python descriptors that do the real work for instances.
what seems awkward here is that you are going for "classes that aren't mapped", which is a standard "high degree of separation of concerns" approach, that's OK, but then you want that same class to be used in a mapped hierarchy, which seems to defeat the whole purpose of separation of concerns. you might want to look into imperative mappings, either declarative with imperative table, or full imperative, if you are going for "the datamodel classes and how they are persisted in a database should be separate concerns" model. |
Beta Was this translation helpful? Give feedback.
-
added #10064. Instantiation of any MappedAsDataclass class that is not mapped is not supported by SQLAlchemy, and behavior is currently undefined. |
Beta Was this translation helpful? Give feedback.
added #10064. Instantiation of any MappedAsDataclass class that is not mapped is not supported by SQLAlchemy, and behavior is currently undefined.