PickleType с изменяемым отслеживанием в SqlAlchemy

У меня есть проект, где я хотел бы сохранить большую структуру (вложенные объекты) в реляционной БД (Postgres). Это часть более крупной структуры, и меня действительно не волнует формат сериализации - я рад, что это blob в столбце - я просто хотел бы иметь возможность сохранять и восстанавливать его довольно быстро.

для моих целей SQLAlchemy PickleType в основном выполняет эту работу. Проблема в том, что я хотел бы, чтобы грязные чеки работали (что-то, что изменяемые типы используются для). Я хотел бы, чтобы они работали не только при изменении информации в путях, но и в границах (которые сидят на другом уровне).

class Group(Base):
    __tablename__ = 'group'

    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)
    paths = Column(types.PickleType)

class Path(object):
    def __init__(self, style, bounds):
        self.style = style
        self.bounds = bounds

class Bound(object):
    def __init__(self, l, t, r, b):
        self.l = l
        self.t = t
        self.r = r
        self.b = b

# this is all fine
g = Group(name='g1', paths=[Path('blah', Bound(1,1,2,3)),
                            Path('other_style', Bound(1,1,2,3)),])
session.add(g)
session.commit()

# so is this
g.name = 'g2'
assert g in session.dirty
session.commit()

# but this won't work without some sort of tracking on the deeper objects
g.paths[0].style = 'something else'
assert g in session.dirty # nope

Я играл с изменчивыми типами, пытаясь заставить его работать, но не повезло. В другом месте я использую изменяемые типы для столбца json, что нормально - таким образом, что кажется проще, потому что с этими классами вам нужно отслеживать изменения объектов внутри объектов.

какие мысли оценил.

1 ответов


прежде всего, как вы поняли, вы должны отслеживать изменения объектов внутри объектов, так как SQLAlchemy не может знать, что внутренний объект изменился. Итак, мы получим это с помощью базового изменяемого объекта, который мы можем использовать для обоих:

class MutableObject(Mutable, object):
    @classmethod
    def coerce(cls, key, value):
        return value

    def __getstate__(self): 
        d = self.__dict__.copy()
        d.pop('_parents', None)
        return d

    def __setstate__(self, state):
        self.__dict__ = state

    def __setattr__(self, name, value):
        object.__setattr__(self, name, value)
        self.changed()


class Path(MutableObject):
    def __init__(self, style, bounds):
        super(MutableObject, self).__init__()
        self.style = style
        self.bounds = bounds


class Bound(MutableObject):
    def __init__(self, l, t, r, b):
        super(MutableObject, self).__init__()
        self.l = l
        self.t = t
        self.r = r
        self.b = b

и нам также нужно отслеживать изменения в списке путей, поэтому мы должны сделать это изменяемым объектом. Однако изменяемый отслеживает изменения в детях, распространяя их на родителей, когда метод changed () вызывается, и текущая реализация в SQLAlchemy, похоже, назначает только родителя кому-то, назначенному как атрибут, а не как элемент последовательности, например, словарь или список. Здесь все усложняется.

Я думаю, что элементы списка должны иметь сам список в качестве родителя, но это не работает по двум причинам: Во-первых, _parents weakdict не может взять список для ключа, а во-вторых, измененный () сигнал не распространяется до самого верха, поэтому мы просто будем отмечать сам список как изменился. Я не на 100% уверен, насколько это правильно, но способ пойти, похоже, назначает родителя списка каждому элементу, поэтому объект group получает вызов flag_modified при изменении элемента. Это должно сработать.

class MutableList(Mutable, list):
    @classmethod
    def coerce(cls, key, value):
        if not isinstance(value, MutableList):
            if isinstance(value, list):
                return MutableList(value)
            value = Mutable.coerce(key, value)

        return value        

    def __setitem__(self, key, value):
        old_value = list.__getitem__(self, key)
        for obj, key in self._parents.items():
            old_value._parents.pop(obj, None)

        list.__setitem__(self, key, value)
        for obj, key in self._parents.items():
            value._parents[obj] = key

        self.changed()

    def __getstate__(self):
        return list(self)

    def __setstate__(self, state):
        self[:] = state

однако, есть еще одна проблема. Родители назначаются прослушиванием вызова в событии "load", поэтому во время инициализации дикт _parents пуст, а дети ничего не получают. Я думаю, может быть, есть какой-то очиститель способ, которым вы можете сделать это, прослушивая событие load, но я решил, что грязный способ сделать это - переназначить родителей, когда элементы извлекаются, поэтому добавьте это:

    def __getitem__(self, key):
        value = list.__getitem__(self, key)

        for obj, key in self._parents.items():
            value._parents[obj] = key

        return value

наконец, мы должны использовать этот MutableList в группе.пути:

class Group(BaseModel):
    __tablename__ = 'group'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, nullable=False)
    paths = db.Column(MutableList.as_mutable(types.PickleType))

и со всем этим ваш тестовый код должен работать:

g = Group(name='g1', paths=[Path('blah', Bound(1,1,2,3)),
                            Path('other_style', Bound(1,1,2,3)),])

session.add(g)
db.session.commit()

g.name = 'g2'
assert g in db.session.dirty
db.session.commit()

g.paths[0].style = 'something else'
assert g in db.session.dirty

честно говоря, я не уверен, насколько безопасно получить это на производстве, и если вам не нужна гибкая схема, вам, вероятно, лучше использовать таблицу и отношения для Path и Bound.