dict-set proxy on self-referential many-to-many SQLAlchemy association

时间:2012-06-23 16:26:27

标签: sqlalchemy

在我的模型中,我有一个基础对象类A,它包含一组属性。 A的每个对象都可以通过多对多关联对象A连接到Context的任何其他对象。这个Context类包含每个连接的密钥。

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)

    # other attributes
    value = Column(Integer)

    def __init__(self, value):
        self.value = value

class Context(Base):
    __tablename__ = "context"
    holder_id = Column(Integer, ForeignKey("a.id"), nullable=False, primary_key=True)
    attachment_id = Column(Integer, ForeignKey("a.id"), nullable=False, primary_key=True)
    key = Column(String, primary_key=True)

    holder = relationship(A,
        primaryjoin=lambda: Context.holder_id==A.id)

    attachment = relationship(A,
        primaryjoin=lambda: Context.attachment_id==A.id)

Context类因此存储“Holder对象a1”形式的3元组,其中包含a2的附件k1

我现在希望在A上有一个智能代理集合,它将这种关系分组在Context.key上,以便我可以按如下方式使用它:

a1 = A(1)
a1.context["key_1"] = set([A(2)])
a1.context["key_2"] = set([A(3), A(4), A(5)])

a1.context["key_1"].add(A(10))

a100 = A(100)
a100.context = {
  "key_1": set([A(101)])
}

如何定义context

我知道在SQLAlchemy文档中有一个用于建模dict-set代理的示例,但不知怎的,我无法让它在自引用的情况下工作。

1 个答案:

答案 0 :(得分:1)

除非我有点消隐(这可能......杜松子酒......)这不能直接在关系上使用关联代理,因为a1.context需要是一个集合,其中每个元素都有一个唯一键,然后集合可以是字典 - 但这里没有这样的集合,因为a1可以有许多具有相同键的Context对象。 Assoc prox将对象集合减少到每个成员对象上的属性集合的简单方法不适用于此。

因此,如果你真的想要这个,并且你的结构不能改变,那就做一个关联代理做的事情,只是以硬编码的方式,即构建一个代理集合!实际上是两个,我想。不是太大的交易,只需要转动曲柄......相当多,请确保为此处的每次操作添加测试:

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
import itertools

Base= declarative_base()

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)

    # other attributes
    value = Column(Integer)

    def __init__(self, value):
        self.value = value

    @property
    def context(self):
        return HolderBySetDict(self)

    @context.setter
    def context(self, dict_):
        toremove = set([ctx for ctx in self.attached_by if ctx.key not in dict_])
        toadd = set([Context(key=k, holder=item) for k, v in dict_.items()
                            for item in itertools.chain(v)])
        self.attached_by.update(toadd)
        self.attached_by.difference_update(toremove)

class HolderBySetDict(object):
    def __init__(self, parent):
        self.parent = parent

    def __iter__(self):
        return iter(self.keys())

    def keys(self):
        return list(set(ctx.key for ctx in self.parent.attached_by))

    def __delitem__(self, key):
        toremove = set([ctx for ctx in self.parent.attached_by if ctx.key == key])
        self.parent.attached_by.difference_update(toremove)

    def __getitem__(self, key):
        return HolderBySet(self.parent, key)

    def __setitem__(self, key, value):
        current = set([ctx for ctx in self.parent.attached_by if ctx.key == key])
        toremove = set([ctx for ctx in current if ctx.holder not in value])
        toadd = set([Context(key=key,holder=v) for v in value if v not in current])
        self.parent.attached_by.update(toadd)
        self.parent.attached_by.difference_update(toremove)

    # exercises !  for the reader !
    #def __contains__(self, key):
    #def values(self):
    #def items(self):
    # ....


class HolderBySet(object):
    def __init__(self, parent, key):
        self.key = key
        self.parent = parent

    def __iter__(self):
        return iter([ctx.holder for ctx
                    in self.parent.attached_by if ctx.key == self.key])

    def update(self, items):
        curr = set([ctx.holder for ctx
                            in self.parent.attached_by if ctx.key==self.key])
        toadd = set(items).difference(curr)
        self.parent.attached_by.update(
                [Context(key=self.key, holder=item) for item in toadd])

    def remove(self, item):
        for ctx in self.parent.attached_by:
            if ctx.key == self.key and ctx.holder is item:
                self.parent.attached_by.remove(ctx)
                break
        else:
            raise ValueError("Value not present")

    def add(self, item):
        for ctx in self.parent.attached_by:
            if ctx.key == self.key and ctx.holder is item:
                break
        else:
            self.parent.attached_by.add(Context(key=self.key, holder=item))

    # more exercises !  for the reader !
    #def __contains__(self, key):
    #def union(self):
    #def intersection(self):
    #def difference(self):
    #def difference_update(self):
    # ....

class Context(Base):
    __tablename__ = "context"
    holder_id = Column(Integer, ForeignKey("a.id"), nullable=False, primary_key=True)
    attachment_id = Column(Integer, ForeignKey("a.id"), nullable=False, primary_key=True)
    key = Column(String, primary_key=True)

    holder = relationship(A,
        primaryjoin=lambda: Context.holder_id==A.id)

    attachment = relationship(A,
        primaryjoin=lambda: Context.attachment_id==A.id,
        backref=backref("attached_by", collection_class=set))

a1 = A(1)
a2 = A(2)
a3, a4, a5 = A(3), A(4), A(5)
a1.context["key_1"] = set([a2])
a1.context["key_2"] = set([a3, a4, a5])

assert set([ctx.holder for ctx in a1.attached_by if ctx.key == "key_1"]) == set([a2])
assert set([ctx.holder for ctx in a1.attached_by if ctx.key == "key_2"]) == set([a3, a4, a5])

a10 = A(10)
a1.context["key_1"].add(a10)
print set([ctx.holder for ctx in a1.attached_by if ctx.key == "key_1"])
assert set([ctx.holder for ctx in a1.attached_by if ctx.key == "key_1"]) == set([a2, a10])

a100 = A(100)
a101 = A(101)
a100.context = {
  "key_1": set([a101])
}
assert set([ctx.holder for ctx in a100.attached_by]) == set([a101])