SQLAlchemy декларативное много-много самообъединение через объект Association
У меня есть таблица Users и таблица Friends, которая отображает пользователей другим пользователям, так как у каждого пользователя может быть много друзей. Это отношение очевидно симметрично: если пользователь A является другом пользователя B, тогда пользователь B также является другом пользователя A, я сохраняю это отношение только один раз. В таблице друзей есть дополнительные поля, кроме двух идентификаторов пользователя, поэтому я должен использовать объект ассоциации.
Я пытаюсь определить это отношение в декларативном стиле в классе Users (который расширяет декларативную базу), но я не могу понять, как это сделать. Я хочу иметь доступ ко всем друзьям данного пользователя через друзей, так что скажите друзьям = bob.friends.
Какой лучший подход для этой проблемы? Я попробовал множество разных настроек для публикации здесь, и никто из них не работал по разным причинам.
EDIT: Моя последняя попытка выглядит так:
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.
Я должен признать, что на этом этапе я полностью смущен из-за многих неудачных попыток и, возможно, совершил более чем одну глупую ошибку в вышеизложенном.
Ответы
Ответ 1
Это конкретное исключение вызвано описанием таблицы более одного раза, либо путем многократного определения сопоставления классов (скажем, в интерактивном интерпретаторе, либо в функции, которая может быть вызвана более одного раза), или путем смешивания класса декларативного стиля отображения с отражением таблицы. В первом случае устраните повторный вызов; запустите новый интерпретатор, если вы делаете это в интерактивном режиме, или устраните дополнительные вызовы функций (возможно, хорошо использовать для объекта singleton/borg).
В последнем случае просто сделайте то, что говорит исключение, добавьте __table_args__ = {'extend_existing': True}
в качестве дополнительной переменной класса в определениях вашего класса. Только сделайте это, если вы действительно уверены, что таблица правильно описана дважды, как при отражении таблицы.
Ответ 2
У меня была эта ошибка с использованием Flask-SQLAlchemy, но другие решения не работали.
Ошибка произошла только на нашем производственном сервере, пока все было нормально на моем компьютере и на тестовом сервере.
У меня был класс "Model", который все мои другие классы базы данных унаследовали от:
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)
Ответ 3
Как упоминалось в комментарии, я предпочитаю расширенную модель, где 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
выше также удалением a User or Friendship or UserFriend
убедитесь, что обе стороны удалены.
Выбор всех друзей будет таким же напряженным, как вы хотите: print user.friends
Реальная проблема с этим решением состоит в том, чтобы обеспечить ровно 2 UserFriend
ссылок для каждого Friendship
. Опять же, при манипулировании объектами из кода это не должно быть проблемой, но база данных может быть потенциально непоследовательна, если кто-то импортирует/обрабатывает некоторые данные непосредственно на стороне SQL.