我有一个项目,我想在关系数据库(Postgres)中存储一个大型结构(嵌套对象)。这是一个更大的结构的一部分,我并不真正关心序列化格式 - 我很高兴它成为一个专栏中的blob - 我只是希望能够相当快地持久化并恢复它。
就我的目的而言,SQLAlchemy PickleType主要完成这项工作。我遇到的问题是,我希望脏检查能够工作(可以使用Mutable Types)。我希望他们不仅可以在路径中更改信息,而且还可以在边界中更改信息(更高级别)。
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
我玩过Mutable类型试图让它工作但没有运气。在其他地方,我确实使用了json列的可变类型,这很好 - 虽然看起来更简单,但是因为使用这些类,你还需要跟踪对象内对象的更改。
任何想法都赞赏。
答案 0 :(得分:6)
首先,正如您所知,您必须跟踪对象内对象的更改,因为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
我们还需要跟踪路径列表中的更改,因此,我们必须将其变为可变对象。但是,Mutable通过在调用changed()方法时将它们传播给父项来跟踪子项中的更改,并且SQLAlchemy中的当前实现似乎只将父项分配给指定为属性的某个人,而不是作为序列的项目,像字典或列表。这是事情变得复杂的地方。
我认为列表项应该将列表本身作为父项,但这不起作用有两个原因:首先,_parents weakdict不能获取键的列表,其次,change()信号不会一直传播到顶部,因此,我们只是将列表本身标记为已更改。我不是100%确定这是多么正确,但是要走的路似乎是将列表的父级分配给每个项目,因此当项目发生更改时,组对象会获得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
然而,这里有最后一个问题。通过监听“加载”事件的呼叫来分配父母,因此在初始化时,_parents dict是空的,并且孩子们没有分配任何内容。我想也许有一些更清洁的方法你可以通过监听加载事件来做到这一点,但我认为这样做的脏方法是在检索项目时重新分配父项,所以,添加:
def __getitem__(self, key):
value = list.__getitem__(self, key)
for obj, key in self._parents.items():
value._parents[obj] = key
return value
最后,我们必须在Group.paths上使用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
坦率地说,我不确定将它投入生产是多么安全,如果你不需要灵活的架构,你可能会更好地使用表和关系路径和绑定。