如何在SQLAlchemy中创建由单个表支持的多级字典?

时间:2014-04-29 02:21:55

标签: python postgresql sqlalchemy

我有一个看起来像这样的表:

values = Table('values',
...
Column('person_id', Unicode(34), ForeignKey('persons.guid')),
Column('type', Text, nullable=False),
Column('name', Text, nullable=False),
Column('params', HSTORE, default={}),
UniqueConstraint('person_id', 'type', 'name'),

我想在relationship对象上配置Person,以便我可以

p = Person()
p.values['a type']['a name'] = {'my': 'params'}

让它按预期工作。

我查看了http://docs.sqlalchemy.org/en/latest/_modules/examples/association/dict_of_sets_with_default.html,它看起来与我想要的类似,包括类似于defaultdict的好功能。看起来它通过关联表,并提供多对多关系。我只想要一对多的关系,并希望每个Person在许多name值的每一个下都有许多type个值。

1 个答案:

答案 0 :(得分:4)

在一个表中执行此操作意味着您只需在现有集合上编写。尝试实现此目的有几种方法,但所有这些方法都需要编写非常精细的嵌套集合类。一种方法是编写此嵌套集合类,以便将所有更改保留为基于列表的简单关系。这是一个简单的方法,但它不是非常有效,因为您必须在更改时重写支持列表或以其他方式搜索它。另一种方法是尝试与集合装饰器探戈。部分内容如下。请注意,我们完全是自定义的,所以我们只是跳过关联代理......必须考虑每个 setitem / getitem 。下面我只使用一个字符串作为值,可以用hstore替换。

from sqlalchemy.orm.collections import MappedCollection, \
                    collection_adapter, collection

class MySubMagicDict(dict):
    def __init__(self, parent, key):
        self.parent = parent
        self.key = key

    def __setitem__(self, key, value):
        self.parent.__setitem__(self.key,
                            Value(type=self.key, name=key, value=value))

class MyMagicDict(MappedCollection):
    def __missing__(self, key):
        d = MySubMagicDict(self, key)
        dict.__setitem__(self, key, d)
        return d

    def __getitem__(self, key):
        d = MySubMagicDict(self, key)
        d.update(
            (v.name, v.value) for v in dict.__getitem__(self, key).values()
        )
        return d

    @collection.internally_instrumented
    def __setitem__(self, key, value, _sa_initiator=None):

        if _sa_initiator is not False:
            executor = collection_adapter(self)
            if executor:
                value = executor.fire_append_event(value, _sa_initiator)

        dict.__setitem__(
            dict.__getitem__(self, key), value.name, value)


    @collection.converter
    def _convert(self, dictlike):
        for incoming_key, value in dictlike.items():
            for name, subvalue in value.items():
                yield Value(type=incoming_key, name=name, value=subvalue)

    @collection.iterator
    def _all_items(self):
        for value in self.values():
            for subvalue in value.values():
                yield subvalue

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

values = Table('values', Base.metadata,
    Column("id", Integer, primary_key=True),
    Column('person_id', Integer, ForeignKey('persons.id')),
    Column('type', Text, nullable=False),
    Column('name', Text, nullable=False),
    Column('value', String)
)
class Value(Base):
    __table__ = values

class Person(Base):
    __tablename__ = 'persons'

    id = Column(Integer, primary_key=True)
    values = relationship("Value", collection_class=
                            lambda: MyMagicDict(lambda item: item.type),
                            cascade="all, delete-orphan"
                        )

e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)

p = Person(values={
    't1': {
        'n1': 't1n1',
        'n2': 't1n2'
    },
    't2': {
        'n1': 't2n1',
        'n2': 't2n2'
    }
})
s.add(p)
s.commit()

assert p.values['t2']['n2'] == 't2n2'
assert p.values['t1']['n1'] == 't1n1'

p.values['t1']['n2'] = 't1n2mod'

s.commit()

assert p.values['t1']['n2'] == 't1n2mod'
assert p.values['t1']['n1'] == 't1n1'