使用SQLAlchemy实现“相关项”功能

时间:2012-06-27 09:04:55

标签: python sqlalchemy

我需要实现“相关项目”功能,即允许来自同一个表的项目以多对多的方式任意地相互链接。类似于新闻网站显示相关文章的内容。

另外,我需要这种关系是双向的,如下所示:

a = Item()
b = Item()
a.related.append(b)
assert a in b.related # True

现在,在SQL级别上我想象这可以通过修改“标准”多对多关系来解决,因此每次建立关联时都会在关联表中插入2条记录,因此(a - > b)和(b - > a)是两个单独的记录。

或者,多对多表的连接条件可以某种方式检查关联的两侧,因此大致而不是... JOIN assoc ON a.id = assoc.left_id ... SQLAlchemy会产生类似... JOIN assoc ON a.id = assoc.left_id OR a.id = assoc.right_id ...

的内容

有没有办法用SQLAlchemy配置它,所以关系的工作方式类似于“普通”的多对多关系?

我可能只是不知道正确的术语 - 我提出的所有内容 - “自我引用”,“双向”,“关联” - 用于描述SQLAlchemy中的其他内容。

2 个答案:

答案 0 :(得分:1)

使用Attribute Events应该可以胜任。请参阅下面的示例代码,其中一些丑陋的代码仅用于避免无休止的递归:

class Item(Base):
    __tablename__ =  "item"

    id = Column(Integer, primary_key=True)
    name = Column(String(255), nullable=False)

    # relationships
    related = relationship('Item', 
            secondary = t_links,
            primaryjoin = (id == t_links.c.from_id),
            secondaryjoin = (id == t_links.c.to_id),
    )

_OTHER_SIDE = set()
from sqlalchemy import event
def Item_related_append_listener(target, value, initiator):
    global _OTHER_SIDE
    if not((target, value) in _OTHER_SIDE):
        _OTHER_SIDE.add((value, target))
        if not target in value.related:
            value.related.append(target)
    else:
        _OTHER_SIDE.remove((target, value))

event.listen(Item.related, 'append', Item_related_append_listener)

# ...
a = Item()
b = Item()
a.related.append(b)
assert a in b.related # True

答案 1 :(得分:0)

为了完整起见,这是我最终得到的代码;监听器方法略有不同,以避免使用全局变量,也有remove事件的监听器。

import sqlalchemy as sa

related_items = sa.Table(
    "related_items",
    Base.metadata,
    sa.Column("id", sa.Integer, primary_key=True),
    sa.Column("from_id", sa.ForeignKey("items.id")),
    sa.Column("to_id", sa.ForeignKey("items.id")),
)


class Item(Base):

    __tablename__ = 'items'
    ...
    related = sa.orm.relationship('Item',
            secondary = related_items,
            primaryjoin = (id == related_items.c.from_id),
            secondaryjoin = (id == related_items.c.to_id),
        )


def item_related_append_listener(target, value, initiator):

    if not hasattr(target, "__related_to__"):
        target.__related_to__ = set()

    target.__related_to__.add(value)

    if target not in getattr(value, "__related_to__", set()):
        value.related.append(target)

sa.event.listen(Item.related, 'append', item_related_append_listener)


def item_related_remove_listener(target, value, initiator):
    if target in value.related:
        value.related.remove(target)

sa.event.listen(Item.related, 'remove', item_related_remove_listener)