es.davy.ai

Preguntas y respuestas de programación confiables

¿Tienes una pregunta?

Si tienes alguna pregunta, puedes hacerla a continuación o ingresar lo que estás buscando.

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?

Tags:  , ,

Answer

  1. Avatar for 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 de Thing1 falla.

    Para solucionar esto, podemos utilizar la función declarative_base de SqlAlchemy en lugar de dataclass y abc.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:

    from sqlalchemy import Column, Integer, ForeignKey, String
    from sqlalchemy.orm import declarative_base, declared_attr, relationship
    
    Base = declarative_base()
    
    class Owner(Base):
        __tablename__ = 'owner'
    
        id = Column(Integer, primary_key=True)
        name = Column(String)
    
    class OwnedThing(Base):
        __tablename__ = 'owned_thing'
    
        id = Column(Integer, primary_key=True)
        owner_id = Column(Integer, ForeignKey('owner.id'))
        owner = relationship(Owner)
    
    class Thing1(OwnedThing):
        __tablename__ = 'thing1'
    
        id = Column(Integer, ForeignKey('owned_thing.id'), primary_key=True)
        name = Column(String)
    
    owner = Owner(name="foo")
    thing = Thing1(name="bar", owner=owner)
    print(thing.owner.name)
    

    En este código, definimos nuestras clases utilizando definiciones de clase Python estándar, heredando de declarative_base en lugar de abc.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 usar relationship para definir la relación entre OwnedThing y Owner, con una restricción de clave externa entre las tablas owned_thing y owner.

    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.

Comments are closed.