2013-10-14 3 views
9

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

Для моих целей 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, который является прекрасным - таким образом, который кажется более простым, хотя, поскольку с этими классами вам нужно также отслеживать изменения объектов в объектах.

Любые мысли оценили.

ответ

5

Прежде всего, как вы поняли, вы должны отслеживать изменения объектов внутри объектов, так как 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, по-видимому, назначает родителя только кому-то, назначенному как атрибут, а не как элемент последовательности, как словарь или список. Здесь все усложняется.

Я думаю, что элементы списка должны иметь список как родительский, но это не работает по двум причинам: во-первых, sparadict не может взять список для ключа, а во-вторых, измененный() сигнал не распространяется до самого верха, поэтому мы просто будем отмечать список как измененный. Я не уверен на 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 

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

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

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

     return value 

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

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 

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

+0

Блестящий ответ, спасибо, что нашли время, чтобы работать со всем этим - я ценю, что это очень нестандартный случай. Это общая идея того, как я думал, что это сработает, но не может понять детали.Мне нужно изучить это немного ближе, чтобы действительно понять это - как вы говорите, может быть, не очень хорошая идея, даже пойти по этому пути. Ваша идея о том, как обойти проблему с списком, очень умна, и я не уверен, что я бы ударил. Еще раз спасибо. Дай тебе щедрость на этом, когда я не на мобильный :) –

+0

Спасибо. Я рад помочь. –

+0

Раздражающе, что минимальная щедрость, которую я могу дать по этому вопросу, теперь 200, потому что раньше у меня была щедрость. Я начал щедрость на совершенно несвязанный вопрос, что я смогу вознаградить вас через 24 часа. –