Кастомизироваать поле orm-модели

Я использую sqlalchemy. У меня есть orm-модель:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import BigInteger
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

ModelWorker = declarative_base(name='ModelGenerator')
Engine = create_engine('sqlite://', echo=False)
Session = sessionmaker(bind=Engine)


class MyField(Column):
    pass


class MyModel(ModelWorker):
    __tablename__ = 'my_model'
    id = Column(BigInteger, primary_key=True)
    value = MyField(BigInteger)


ModelWorker.metadata.create_all(Engine)

model = MyModel(id=1, value=100)
session = Session()
session.add(model)
session.commit()

Я хочу кастомизировать поле value, чтобы при присваиванию ему значения оно, допустим, выполняло добавляло к нему 10 (model = MyModel(id=1, value=100);model.value == 110). Мне надо как-то изменить класс MyField или MyModel для этого, и как это сделать?


Ответы (1 шт):

Автор решения: Michael Tetelev

Ответ несколько сложнее, чем я думал. С одной стороны, он прост:

class MyModel(ModelWorker):
    __tablename__ = 'my_model'
    id = Column(BigInteger, primary_key=True)
    value = MyField(BigInteger)

    def __init__(self, *args, **kwargs):
        if 'value' in kwargs:
            # Do code
            kwargs[name] += 10
        super().__init__(*args, **kwargs)

И для разового использования вполне годится. Для использования же в разных моделях каждый раз писать название полей очень плохо и некрасиво. Поэтому можно сделать так:

class BaseModel(ModelWorker):
    __abstract__ = True

    def __init__(self, *args, **kwargs):
        for (name, value) in kwargs.items():
            field_class = getattr(self, name, None)
            if isinstance(field_class, MyField):
                # Do code
                kwargs[name] += 10
        super().__init__(*args, **kwargs)


class MyModel(BaseModel):
    __tablename__ = 'my_model'
    id = Column(BigInteger, primary_key=True)
    value = MyField(BigInteger)
        

И тут есть одна неожиданная особенность. Проблема возникает, когда мы вызываем isinstance(field_class, MyField), и заключается в том, что при инициализации класса все поля переводятся Column -> InstrumentedAttribute:

>>> MyModel.value
# <sqlalchemy.orm.attributes.InstrumentedAttribute object at 0x7f01518b2ea0>

Для получения оригинального списка столбцов нужно обращаться к MyModel.__table__.columns:

class BaseModel(ModelWorker):
    __abstract__ = True

    def __init__(self, *args, **kwargs):
        for (name, value) in kwargs.items():
            field_class = self.__table__.columns.get(name, None)
            if isinstance(field_class, MyField):
                # Do code
                kwargs[name] += 10
        super().__init__(*args, **kwargs)

class MyModel(BaseModel):
    __tablename__ = 'my_model'
    id = Column(BigInteger, primary_key=True)
    value = MyField(BigInteger)

model = MyModel(id=1, value=1)
my.value  # 11
→ Ссылка