如何在SQLAlchemy ORM中实现自引用的多对多关系,反向引用相同属性?
我正在尝试在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