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.