Как я могу получить самосвязывающиеся отношения «многие ко многим» в SQLAlchemy ORM с обратной ссылкой на один и тот же атрибут? - PullRequest
24 голосов
/ 02 февраля 2012

Я пытаюсь реализовать самореферентное отношение «многие ко многим», используя декларативное в SQLAlchemy.

Отношения представляют собой дружбу между двумя пользователями. В Интернете я узнал (как в документации, так и в Google), как создать самореферентные отношения m2m, где роли как-то различаются. Это означает, что в этих отношениях m2m UserA является, например, боссом UserB, поэтому он перечисляет его под атрибутом «подчиненные» или что у вас есть. Точно так же 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=??????
                       )

Это имеет смысл, потому что, если А дружит с Б, роли в отношениях одинаковы, и если я вызову друзей Б, я должен получить список с А в нем. Это проблемный код в полном объеме:

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 ]

23 голосов
/ 03 февраля 2012

Вот подход 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
10 голосов
/ 23 июля 2015

Мне нужно было решить эту же проблему, и я довольно много перепутал с самореферентными отношениями «многие ко многим», где я также подклассифицировал класс User с классом Friend и столкнулся с 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
...