SQLAlchemy декларативное самосоединение «многие ко многим» через объект «Ассоциация»

У меня есть таблица «Пользователи» и таблица «Друзья», которая сопоставляет пользователей с другими пользователями, поскольку у каждого пользователя может быть много друзей. Это отношение, очевидно, симметрично: если пользователь A является другом пользователя B, то пользователь B также является другом пользователя A, я сохраняю это отношение только один раз. В таблице друзей есть дополнительные поля, помимо двух идентификаторов пользователя, поэтому мне приходится использовать объект ассоциации.

Я пытаюсь определить эту связь в декларативном стиле в классе Users (который расширяет декларативную базу), но я не могу понять, как это сделать. Я хочу иметь доступ ко всем друзьям данного пользователя через друзья свойства, так сказать, друзья = bob.friends.

Каков наилучший подход к этой проблеме? Я пробовал много разных настроек, чтобы опубликовать здесь, и ни одна из них не работала по разным причинам.

РЕДАКТИРОВАТЬ: Моя последняя попытка выглядит так:

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)

    # Relationships
    friends1 = relationship('Friends', primaryjoin=lambda: id==Friends.friend1ID)
    friends2 = relationship('Friends', primaryjoin=lambda: id==Friends.friend2ID)


class Friends(Base):
    __tablename__ = 'friends'
    id = Column(Integer, primary_key=True)
    friend1ID = Column(Integer, ForeignKey('users.id') )
    friend2ID = Column(Integer, ForeignKey('users.id') )
    status = Column(Integer)

    # Relationships
    vriend1 = relationship('Student', primaryjoin=student2ID==Student.id)
    vriend2 = relationship('Student', primaryjoin=student1ID==Student.id)

Однако это приводит к следующей ошибке:

InvalidRequestError: Table 'users' is already defined for this MetaData instance.  Specify 'extend_existing=True' to redefine options and columns on an existing Table object.

Я должен признать, что на данный момент я совершенно сбит с толку из-за множества неудачных попыток и, возможно, сделал не одну глупую ошибку в вышеизложенном.


person Janny    schedule 05.09.2011    source источник
comment
Какие дополнительные поля? Мне интересно, являются ли они общими для обоих друзей или directional.   -  person van    schedule 05.09.2011
comment
Посмотрите на другой вопрос на SO [stackoverflow.com/questions/7120774/: он имеет аналогичную структуру, и Гэри пытается решить симметрию с помощью представления. Лично я не большой поклонник этой идеи, но вы можете попробовать.   -  person van    schedule 05.09.2011
comment
Дополнительное поле — это поле состояния, которое указывает, был ли 1) запрос на добавление в друзья только что отправлен или б) отправлен и принят. В будущем мне могут понадобиться дополнительные поля. Подход Гэри действительно немного уродлив, и мне бы очень хотелось найти лучшее решение. Кто-нибудь?   -  person Janny    schedule 05.09.2011
comment
запрос принят? принят кем, либо другом? это не похоже на симметричные отношения для меня. Возможно, вы захотите либо исключить дополнительное поле, чтобы отношение друга действительно было симметричным, либо переосмыслить требование симметричности и просто иметь две строки: a является другом b, а b является другом a, чтобы вы могли более элегантно зафиксировать асимметрию. .   -  person SingleNegationElimination    schedule 05.09.2011
comment
это единственные модели, которые вы определяете, или у вас есть другие модели, определенные где-то еще в вашем проекте?   -  person Cagatay Kalan    schedule 25.12.2011


Ответы (3)


Это конкретное исключение вызвано описанием таблицы более одного раза, либо повторным определением сопоставления классов (скажем, в интерактивном интерпретаторе или в функции, которую можно вызывать более одного раза), либо смешиванием сопоставлений классов в декларативном стиле с таблицей. отражение. В первом случае исключите повторный вызов; запустите новый интерпретатор, если вы делаете это в интерактивном режиме, или исключите дополнительные вызовы функций (возможно, хорошее использование для объекта singleton/borg).

В последнем случае просто сделайте то, что говорит исключение, добавьте __table_args__ = {'extend_existing': True} в качестве дополнительной переменной класса в определениях вашего класса. Делайте это только в том случае, если вы действительно уверены, что таблица описывается правильно дважды, как в случае отражения таблицы.

person SingleNegationElimination    schedule 05.09.2011
comment
Я определяю два сопоставления классов/классов в одном файле с именем User.py и импортирую его из любого места, где мне нужно работать с этими моделями. Это неправильно? Я проверил и дважды проверил циклы импорта в своем коде, но не смог найти... - person Janny; 06.09.2011
comment
поместите оператор print непосредственно перед определением класса и посмотрите, импортируется ли он более одного раза. Если это так, запустите в режиме отладки или распечатайте трассировку стека, чтобы узнать, почему модуль загружается/импортируется дважды. - person van; 06.09.2011
comment
Чтобы уточнить предложение Вана, добавьте print "models imported as", __name__ - person SingleNegationElimination; 06.09.2011
comment
Спасибо. Мне нужно было переопределить класс в интерпретаторе, и я просмотрел ошибку, к удивлению, не смог найти пример того, как это сделать. - person Dark Star1; 18.05.2017

У меня была эта ошибка с использованием Flask-SQLAlchemy, но другие решения не работали.

Ошибка возникла только на нашем рабочем сервере, в то время как на моем компьютере и на тестовом сервере все работало нормально.

У меня был класс «Модель», от которого унаследованы все остальные классы моей базы данных:

class Model(db.Model):

    id = db.Column(db.Integer, primary_key=True)

По какой-то причине ORM дала классам, унаследованным от этого класса, то же имя table, что и у этого класса. То есть для каждого класса он пытался сделать таблицу для него, называемую таблицей «модель».

Решение состояло в том, чтобы явно назвать дочерние таблицы с помощью переменной класса 'tablename':

class Client(Model):

    __tablename__ = "client"

    email = db.Column(db.String)
    name = db.Column(db.String)
    address = db.Column(db.String)
    postcode = db.Column(db.String)
person public static void    schedule 25.02.2016

Как упоминалось в комментарии, я предпочитаю расширенную модель, в которой Friendship — это отдельная сущность, а связи между друзьями — это отдельные сущности. Таким образом, можно хранить как симметричные, так и асимметричные свойства (например, что один человек думает о другом). Таким образом, приведенная ниже модель должна показать вам, что я имею в виду:

...
class User(Base):
    __tablename__ =  "user"

    id = Column(Integer, primary_key=True)
    name = Column(String(255), nullable=False)

    # relationships
    friends = relationship('UserFriend', backref='user',
            # ensure that deletes are propagated
            cascade='save-update, merge, delete',
    )

class Friendship(Base):
    __tablename__ =  "friendship"

    id = Column(Integer, primary_key=True)
    # additional info symmetrical (common for both sides)
    status = Column(String(255), nullable=False)
    # @note: also could store a link to a Friend who requested a friendship

    # relationships
    parties = relationship('UserFriend', 
            back_populates='friendship',
            # ensure that deletes are propagated both ways
            cascade='save-update, merge, delete',
        )

class UserFriend(Base):
    __tablename__ =  "user_friend"

    id = Column(Integer, primary_key=True)
    friendship_id = Column(Integer, ForeignKey(Friendship.id), nullable=False)
    user_id = Column(Integer, ForeignKey(User.id), nullable=False)
    # additional info assymmetrical (different for each side)
    comment = Column(String(255), nullable=False)
    # @note: one could also add 1-N relationship where one user might store
    # many different notes and comments for another user (a friend)
    # ...

    # relationships
    friendship = relationship(Friendship,
            back_populates='parties',
            # ensure that deletes are propagated both ways
            cascade='save-update, merge, delete',
        )

    @property
    def other_party(self):
        return (self.friendship.parties[0] 
                if self.friendship.parties[0] != self else
                self.friendship.parties[1]
                )

    def add_friend(self, other_user, status, comment1, comment2):
        add_friendship(status, self, comment1, other_user, comment2)

# helper method to add a friendship
def add_friendship(status, usr1, comment1, usr2, comment2):
    """ Adds new link to a session.  """
    pl = Friendship(status=status)
    pl.parties.append(UserFriend(user=usr1, comment=comment1))
    pl.parties.append(UserFriend(user=usr2, comment=comment2))
    return pl

Таким образом, добавлять дружбу довольно просто.
Как и обновлять любые ее атрибуты. Вы можете создать дополнительные вспомогательные методы, такие как add_friend.
С приведенной выше конфигурацией cascade также удаление User or Friendship or UserFriend обеспечит удаление обеих сторон.
Выбор всех друзей так просто, как вы хотите: print user.friends

Настоящая проблема с этим решением состоит в том, чтобы гарантировать наличие ровно 2 ссылок UserFriend для каждого Friendship. Опять же, при манипулировании объектами из кода это не должно быть проблемой, но база данных потенциально может быть несогласованной, если кто-то импортирует/манипулирует некоторыми данными непосредственно на стороне SQL.

person van    schedule 06.09.2011