如何在 SQLAlchemy ORM 上实现对同一属性的自引用多对多关系?

Posted

技术标签:

【中文标题】如何在 SQLAlchemy ORM 上实现对同一属性的自引用多对多关系?【英文标题】:How can I achieve a self-referencing many-to-many relationship on the SQLAlchemy ORM back referencing to the same attribute? 【发布时间】:2012-02-25 09:05:20 【问题描述】:

我正在尝试使用 SQLAlchemy 上的声明性来实现自引用多对多关系。

关系代表两个用户之间的友谊。在线我发现(在文档和谷歌中)如何建立一个自我参照的 m2m 关系,以某种方式区分角色。这意味着在这种 m2m 关系中,用户 A 是,例如,用户 B 的老板,所以他将他列为“下属”属性或你有什么。以同样的方式,UserB 将 UserA 列在“上级”下。

这没有问题,因为我们可以这样声明同一张表的反向引用:

subordinates = relationship('User', backref='superiors')

当然,“superiors”属性在类中并不显式。

无论如何,这是我的问题:如果我想反向引用到我调用反向引用的同一属性怎么办?像这样:

friends = relationship('User',
                       secondary=friendship, #this is the table that breaks the m2m
                       primaryjoin=id==friendship.c.friend_a_id,
                       secondaryjoin=id==friendship.c.friend_b_id
                       backref=??????
                       )

这是有道理的,因为如果 A 与 B 成为朋友,则关系角色是相同的,如果我调用 B 的朋友,我应该得到一个包含 A 的列表。这是完整的有问题的代码:

friendship = Table(
    'friendships', Base.metadata,
    Column('friend_a_id', Integer, ForeignKey('users.id'), primary_key=True),
    Column('friend_b_id', Integer, ForeignKey('users.id'), primary_key=True)
)

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)

    friends = relationship('User',
                           secondary=friendship,
                           primaryjoin=id==friendship.c.friend_a_id,
                           secondaryjoin=id==friendship.c.friend_b_id,
                           #HELP NEEDED HERE
                           )

抱歉,如果文字过多,我只想尽可能明确地说明这一点。我似乎无法在网上找到任何参考资料。

【问题讨论】:

【参考方案1】:

这是我今天早些时候在邮件列表中暗示的 UNION 方法。

from sqlalchemy import Integer, Table, Column, ForeignKey, \
    create_engine, String, select
from sqlalchemy.orm import Session, relationship
from sqlalchemy.ext.declarative import declarative_base

Base= declarative_base()

friendship = Table(
    'friendships', Base.metadata,
    Column('friend_a_id', Integer, ForeignKey('users.id'), 
                                        primary_key=True),
    Column('friend_b_id', Integer, ForeignKey('users.id'), 
                                        primary_key=True)
)


class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)

    # this relationship is used for persistence
    friends = relationship("User", secondary=friendship, 
                           primaryjoin=id==friendship.c.friend_a_id,
                           secondaryjoin=id==friendship.c.friend_b_id,
    )

    def __repr__(self):
        return "User(%r)" % self.name

# this relationship is viewonly and selects across the union of all
# friends
friendship_union = select([
                        friendship.c.friend_a_id, 
                        friendship.c.friend_b_id
                        ]).union(
                            select([
                                friendship.c.friend_b_id, 
                                friendship.c.friend_a_id]
                            )
                    ).alias()
User.all_friends = relationship('User',
                       secondary=friendship_union,
                       primaryjoin=User.id==friendship_union.c.friend_a_id,
                       secondaryjoin=User.id==friendship_union.c.friend_b_id,
                       viewonly=True) 

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

u1, u2, u3, u4, u5 = User(name='u1'), User(name='u2'), \
                    User(name='u3'), User(name='u4'), User(name='u5')

u1.friends = [u2, u3]
u4.friends = [u2, u5]
u3.friends.append(u5)
s.add_all([u1, u2, u3, u4, u5])
s.commit()

print u2.all_friends
print u5.all_friends

【讨论】:

这似乎有点容易出错:您可能会不小心追加到all_friends 并且不会收到任何警告。有什么建议吗? 另外,这允许使用交换 id 的重复友谊(如 1, 22, 1)。您可以设置一个 id 大于另一个 id 的约束,但是您需要跟踪哪些用户可以附加到哪些用户 friends 属性。 viewonly=True 与 Python 中集合的行为无关。如果你真的关心追加到这个集合,你可以使用 collection_cls 并应用一个列表或集合类型,它覆盖了变异方法以抛出 NotImplementedError 或类似的。 至于1->2 + 2->1,不同的系统对此有不同的看法。在上面的示例中,它不会直接导致任何“问题”,因为 User.all_friends 在填充时会根据身份对 User 对象进行重复数据删除。现实世界的“朋友”系统可能希望将附加数据应用于每个“朋友”关系——用户 1 可能会说他通过“工作”认识用户 2,而用户 2 可能通过“学校”报告认识用户 1,并且系统可能想要存储这两个事实,例如这是一个有向图。 (续) 如果 OTOH 你想将它限制在任何两个用户对象之间的一个边缘,它可以像应用 SQL 级约束一样简单(尽管这需要一个 SELECT-per-insert,我可能关注性能),在 Python 端,您只需使用 append event 在追加时检查“all_friends”集合【参考方案2】:

我需要解决同样的问题,并且在自引用多对多关系中搞砸了很多,其中我还用 Friend 类子类化 User 类并遇到 sqlalchemy.orm.exc.FlushError。最后,我没有创建自引用的多对多关系,而是使用连接表(或辅助表)创建了自引用的一对多关系。

如果您考虑一下,对于自引用对象,一对多就是多对多。它解决了原始问题中的backref问题。

如果您想查看它的实际效果,我也有一个 gisted working example。现在看起来像 github 格式的 gists 包含 ipython 笔记本。整洁。

friendship = Table(
    'friendships', Base.metadata,
    Column('user_id', Integer, ForeignKey('users.id'), index=True),
    Column('friend_id', Integer, ForeignKey('users.id')),
    UniqueConstraint('user_id', 'friend_id', name='unique_friendships'))


class User(Base):
    __tablename__ = 'users'

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

    friends = relationship('User',
                           secondary=friendship,
                           primaryjoin=id==friendship.c.user_id,
                           secondaryjoin=id==friendship.c.friend_id)

    def befriend(self, friend):
        if friend not in self.friends:
            self.friends.append(friend)
            friend.friends.append(self)

    def unfriend(self, friend):
        if friend in self.friends:
            self.friends.remove(friend)
            friend.friends.remove(self)

    def __repr__(self):
        return '<User(name=|%s|)>' % self.name

【讨论】:

以上是关于如何在 SQLAlchemy ORM 上实现对同一属性的自引用多对多关系?的主要内容,如果未能解决你的问题,请参考以下文章

如何拥有一个包含数千个表的数据库,这些表具有不同数量的列,这些列在 Django / SQLAlchemy ORM 中都是同一类?

ORM框架之SQLALchemy

Mac上实现对Python的版本切换

SQLAlchemy:如何在删除时禁用 ORM 级外键处理?

如何理解python的sqlalchemy这种orm框架

如何在db更新后获取SQLAlchemy ORM对象的先前状态?