如何在SQLAlchemy ORM中实现自引用的多对多关系,反向引用相同属性?

36 投票
2 回答
15835 浏览
提问于 2025-04-17 12:12

我正在尝试在SQLAlchemy中实现一个自引用的多对多关系。

这个关系表示两个用户之间的友谊。在网上,我找到了一些资料(包括文档和谷歌搜索),关于如何创建一个自引用的多对多关系,其中角色是有区别的。这意味着在这种多对多关系中,用户A可能是用户B的上司,所以他会把用户B列在一个叫“下属”的属性下。反过来,用户B也会把用户A列在“上司”下。

这样做没有问题,因为我们可以通过这种方式在同一个表中声明一个反向引用:

subordinates = relationship('User', backref='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
                           )

抱歉文字有点多,我只是想尽量详细说明这个问题。我在网上找不到相关的参考资料。

2 个回答

14

我之前也遇到过类似的问题,尝试了很多次自引用的多对多关系。在这个过程中,我还把User类进行了子类化,创建了一个Friend类,结果碰到了sqlalchemy.orm.exc.FlushError的错误。最后,我决定不再使用自引用的多对多关系,而是用一个连接表(或者说是辅助表)来创建自引用的一对多关系。

其实想一想,自引用的对象中,一对多关系实际上就是多对多关系。这解决了原问题中的反向引用问题。

如果你想看看具体的例子,我还有一个在gist上的工作示例,可以看看它是怎么运作的。另外,似乎现在github对包含ipython笔记本的gist进行了格式化,挺不错的。

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
30

这是我今天早些时候在邮件列表中提到的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

撰写回答