Как я могу достичь взаимосвязи "многие ко многим" в ORM SQLAlchemy, ссылаясь на один и тот же атрибут?
Я пытаюсь реализовать самореферентное отношение "многие ко многим", используя декларативное на SQLAlchemy.
Отношения представляют дружбу между двумя пользователями. В Интернете я нашел (как в документации, так и в Google), как сделать самореферентную связь m2m, где каким-то образом дифференцируются роли. Это означает, что в этих отношениях m2m UserA является, например, боссом UserB, поэтому он перечисляет его под атрибутом "подчиненные" или тем, что у вас есть. Точно так же UserB перечисляет UserA под "начальством".
Это не проблема, потому что мы можем объявить backref в той же таблице следующим образом:
subordinates = relationship('User', backref='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
)
Извините, если это слишком много текста, я просто хочу быть таким явным, как я могу. Кажется, я не могу найти какой-либо справочный материал в Интернете.
Ответы
Ответ 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
Ответ 2
Мне нужно было решить эту же проблему и довольно много перепуталось с самореляционными отношениями "многие ко многим", в которых я также подклассифицировал класс User
классом Friend
и работал в sqlalchemy.orm.exc.FlushError
. В конце концов, вместо того, чтобы создавать самореляционные отношения "многие-ко-многим", я создал самореферентное отношение "один ко многим", используя таблицу соединений (или вторичную таблицу).
Если вы думаете об этом, имея собственные ссылочные объекты, один-ко-многим IS-ко многим. Он решил вопрос о backref в исходном вопросе.
У меня также есть gisted рабочий пример, если вы хотите увидеть его в действии. Также похоже, что github теперь форматирует gists, содержащий ipython notebooks. Ухоженная.
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