Comportamiento de la clase de datos de SqlAlchemy y mixins declarativos: AttributeError: no se puede establecer el atributo.
Tengo un conjunto de clases que redacté como dataclasses de Python puro. Aquí hay un ejemplo simplificado que se comporta como esperaría:
from dataclasses import dataclass
from abc import ABC
@dataclass
class Owner:
name: str = ""
@dataclass
class OwnedThing(ABC):
owner: Owner = None
@dataclass
class Thing1(OwnedThing):
name: str = ""
owner = Owner(name="foo")
thing = Thing1( name="bar", owner=owner)
print(thing.owner.name)
Aquí tenemos una clase base que establece que una serie de clases derivadas tendrán todas un objeto “owner”. Asignar el propietario es sencillo.
Cuando traté de agregar SqlAlchemy para la persistencia, me gustaría mantener la estructura general de herencia para que no tenga que repetir esta relación owner-thing para todas las variedades de owned things. Sin embargo, cuando lo hago, ya no puedo asignar una instancia de clase propietaria al atributo .owner:
from dataclasses import dataclass,field
from abc import ABC
from sqlalchemy.orm import declarative_mixin, declared_attr, relationship
from sqlalchemy import Column, ForeignKey
@dataclass
class Owner:
name: str = ""
@declarative_mixin
@dataclass
class OwnedThing(ABC):
owner_id: int = field(init=False, default=None)
owner: Owner = None
@declared_attr
def owner_id(cls):
return Column('owner_id', ForeignKey('owner.id'))
@declared_attr
def owner(cls):
return relationship(Owner)
@dataclass
class Thing1(OwnedThing):
name: str = ""
owner = Owner(name="foo")
thing = Thing1( name="bar", owner=owner)
print(thing.owner.name)
Esto da como resultado esta traza de stack:
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/dataclasses.py:658: SAWarning: Unmanaged access of declarative attribute owner_id from non-mapped class OwnedThing
default = getattr(cls, a_name, MISSING)
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/dataclasses.py:658: SAWarning: Unmanaged access of declarative attribute owner from non-mapped class OwnedThing
default = getattr(cls, a_name, MISSING)
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/dataclasses.py:842: SAWarning: Unmanaged access of declarative attribute owner_id from non-mapped class OwnedThing
if isinstance(getattr(cls, f.name, None), Field):
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/dataclasses.py:842: SAWarning: Unmanaged access of declarative attribute owner from non-mapped class OwnedThing
if isinstance(getattr(cls, f.name, None), Field):
Traceback (most recent call last):
File "test_case_sql.py", line 30, in <module>
thing = Thing1( name="bar", owner=owner)
File "<string>", line 2, in __init__
AttributeError: can't set attribute
Tenga en cuenta que cuando omito tener una clase base, las cosas funcionan como esperaría, sin errores:
from dataclasses import dataclass,field
from abc import ABC
from sqlalchemy.orm import declarative_mixin, declared_attr, relationship
from sqlalchemy import Column, ForeignKey,Integer
@dataclass
class Owner:
name: str = ""
@dataclass
class Thing1:
owner_id: int = Column(Integer)
owner: Owner = relationship("Owner")
name: str = ""
owner = Owner(name="foo")
thing = Thing1( name="bar", owner=owner)
print(thing.owner.name)
No sé lo suficiente sobre los entresijos de SqlAlchemy para entender qué estoy haciendo mal, y no he visto a nadie más encontrar este problema antes. ¿Alguien puede ayudarme a seguir usando data-classes y mixins?
davy.ai
El problema aquí es que SqlAlchemy tiene dificultades para mapear la jerarquía de dataclass a un esquema de base de datos. Específicamente, la clase
OwnedThing
no está siendo reconocida como una clase mapeada, por lo que intentar asignar un propietario a una instancia deThing1
falla.Para solucionar esto, podemos utilizar la función
declarative_base
de SqlAlchemy en lugar dedataclass
yabc.ABC
. Esto nos permite definir nuestra jerarquía de clase utilizando la herencia y tenerla correctamente mapeada al esquema de base de datos.Aquí hay un ejemplo de cómo podemos reescribir el código para usar
declarative_base
:En este código, definimos nuestras clases utilizando definiciones de clase Python estándar, heredando de
declarative_base
en lugar deabc.ABC
. Utilizamos atributos de clase para definir el esquema de base de datos para cada clase, en lugar de usar campos de dataclass.Con esta configuración, podemos utilizar la herencia para definir la relación propietario-objeto una vez, y se aplicará a todas las clases que hereden de
OwnedThing
. Luego podemos usarrelationship
para definir la relación entreOwnedThing
yOwner
, con una restricción de clave externa entre las tablasowned_thing
yowner
.Al utilizar
declarative_base
, podemos aprovechar al máximo las características proporcionadas por SqlAlchemy, como las relaciones y las restricciones, mientras seguimos utilizando la sintaxis estándar de Python para la definición de clases.