Python SqlAlchemy中具有可变跟踪的PickleType

Python SqlAlchemy中具有可变跟踪的PickleType,python,postgresql,sqlalchemy,persistence,mutable,Python,Postgresql,Sqlalchemy,Persistence,Mutable,我有一个项目,我想在关系数据库(Postgres)中存储一个大型结构(嵌套对象)。它是更大结构的一部分,我并不真正关心序列化格式——我很高兴它是列中的一个blob——我只希望能够相当快地持久化和恢复它 就我而言,SQLAlchemy类型主要完成这项工作。我所面临的问题是,我希望脏检查能够正常工作(可变类型用于此目的)。我希望它们不仅在我更改路径中的信息时起作用,而且在边界(位于另一层)中也起作用 我曾经和可变类型的人打过交道,试图让它工作起来,但没有任何运气。在其他地方,我确实为json列使用了

我有一个项目,我想在关系数据库(Postgres)中存储一个大型结构(嵌套对象)。它是更大结构的一部分,我并不真正关心序列化格式——我很高兴它是列中的一个blob——我只希望能够相当快地持久化和恢复它

就我而言,SQLAlchemy类型主要完成这项工作。我所面临的问题是,我希望脏检查能够正常工作(可变类型用于此目的)。我希望它们不仅在我更改路径中的信息时起作用,而且在边界(位于另一层)中也起作用

我曾经和可变类型的人打过交道,试图让它工作起来,但没有任何运气。在其他地方,我确实为json列使用了可变类型,这很好,但方式似乎更简单,因为使用这些类,您也需要跟踪对象中对象的更改


任何想法都值得欣赏。

首先,正如您所意识到的,您必须跟踪对象中对象的变化,因为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中的当前实现似乎只将父项分配给作为属性而不是作为序列项(如字典或列表)分配的人。这就是事情变得复杂的地方

我认为列表项应该将列表本身作为父项,但这不起作用,原因有两个:首先,_parentsweakdict不能将列表作为键,其次,changed()信号不会一直传播到顶部,因此,我们只会将列表本身标记为changed。我不确定这是否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”事件的调用进行分配,因此在初始化时,\ u parents dict为空,子对象未分配任何内容。我认为,通过监听load事件,也许有更干净的方法可以做到这一点,但我认为最糟糕的方法是在检索项目时重新分配父项,因此,添加以下内容:

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

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

        return value
最后,我们必须在Group.path上使用该可变列表:

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

坦率地说,我不确定在生产环境中使用该模式有多安全,如果您不需要灵活的模式,那么最好使用表和关系作为路径和绑定。

回答得很好,感谢您花时间解决所有这些问题-我很欣赏这是一个非常不标准的案例。这是我认为它将如何工作的一般想法,但无法完全掌握细节。我需要更深入地研究这一点,才能真正理解它——正如你所说,走这条路可能不是个好主意。你关于如何解决列表问题的想法非常聪明,我不确定我是否会想到。再次感谢。我不在手机上的时候会给你一笔赏金:)谢谢。我很乐意帮忙。令人烦恼的是,在这个问题上我能给的最低悬赏现在是200,因为我以前在这里有悬赏。我已经开始悬赏一个完全无关的问题,我会在24小时内奖励你。别担心。正如我所说,我很乐意帮忙。我信守诺言。而且,我已经开始悬赏了,所以把它给别人是愚蠢的:)
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