如何在SQLAlchemy ORM中实现自引用的多对多关系,并将其反向引用到同一属性?

35

我正在尝试使用SQLAlchemy的声明式语法实现自引用的多对多关系。

这个关系表示两个用户之间的友谊。在网上我发现了(无论是在文档还是Google中)如何建立一个自引用的m2m关系,其中一些角色有所区别。这意味着在这个m2m关系中,例如UserA是UserB的老板,因此他在“下属”属性或其他属性下列出他。同样,UserB在“上司”属性下列出UserA。

这并不成问题,因为我们可以以以下方式在同一张表中声明backref:

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

当然,在这个类中,'superiors'属性是没有显式声明的。

无论如何,这是我的问题:如果我想要在反向引用(backref)时引用正在调用backref的同一属性,该怎么办?就像这样:

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
                           )

如果这段文字太长的话,抱歉了。我只是想尽可能明确地描述问题。我在网上找不到任何参考资料。

2个回答

30

这是我早些时候在邮件列表中提到的联合方法。

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,而且你不会得到任何警告。有什么建议吗? - Vladimir Keleshev
此外,这允许使用交换的ID进行重复的友谊(例如1,22,1)。您可以设置一个约束条件,即一个ID必须大于另一个ID,但是这样您需要跟踪哪些用户可以附加到哪些用户的“朋友”属性。 - Vladimir Keleshev
1
viewonly=True对Python中集合的行为没有影响。如果您真的担心对该集合进行附加操作,可以使用collection_cls并应用重写了变异方法以抛出NotImplementedError或类似异常的列表或集合类型。 - zzzeek
就1->2 + 2->1而言,不同的系统可能会有不同的看法。在上面的例子中,它不会直接导致任何“问题”,因为当User.all_friends填充时,它将根据身份对用户对象进行去重。一个真实的“朋友”系统可能希望在每个“朋友”关系上应用附加数据——用户1可能会说他通过“工作”认识用户2,而用户2可能会报告通过“学校”认识用户1,系统可能希望存储这两个事实,例如这是一个有向图。 - zzzeek
1
如果您想在任意两个用户对象之间仅限制为一个边缘,那么最简单的方法是应用一个SQL级别的约束(尽管这将需要每次插入都进行一次SELECT,而我可能会担心性能),并且在Python侧,在使用附加事件时只需检查“all_friends”集合即可。 - zzzeek
显示剩余4条评论

15

我曾经遇到同样的问题,并在自引用的多对多关系上浪费了很多时间,其中我还使用Friend类继承了User类,并遇到了sqlalchemy.orm.exc.FlushError。最后,我没有创建自引用的多对多关系,而是创建了一个使用联接表(或次要表)的自引用的一对多关系。

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

如果您想看到它的实际效果,我还有一个有效的示例。此外,看起来Github现在可以格式化包含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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接