通过关联对象在SQLAlchemy声明式中实现多对多自连接

11 投票
3 回答
11947 浏览
提问于 2025-04-17 01:29

我有一个用户表(Users)和一个朋友表(Friends),这个朋友表用来记录用户之间的关系,因为每个用户可以有很多朋友。这个关系是对称的:如果用户A是用户B的朋友,那么用户B也一定是用户A的朋友,所以我只需要存储一次这个关系。朋友表除了有两个用户ID外,还有其他一些字段,所以我需要使用一个关联对象。

我想在用户类(Users类,继承了声明性基础)中以声明的方式定义这个关系,但我不知道该怎么做。我希望能够通过一个属性来访问某个用户的所有朋友,比如说friends = 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.

我必须承认,经过这么多次失败的尝试,我现在完全搞糊涂了,可能在上面的代码中犯了不止一个低级错误。

3 个回答

1

正如评论中提到的,我更喜欢扩展模型,在这个模型中,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(用户)、Friendship(友谊)或 UserFriend(用户友谊)时,确保两边的关系都会被删除。
选择所有朋友也很简单:只需使用 print user.friends 就可以了。

这个解决方案真正的问题在于确保每个 Friendship 只有两个 UserFriend 连接。再次强调,当你在代码中操作这些对象时,这应该不是问题,但如果有人直接在 SQL 里导入或操作数据,数据库可能会出现不一致的情况。

2

我在使用Flask-SQLAlchemy的时候遇到了一个错误,但其他的解决办法都没用。

这个错误只在我们的生产服务器上出现,而在我的电脑和测试服务器上都没问题。

我有一个叫做'模型'的类,所有其他的数据库类都是从这个类继承的:

class Model(db.Model):

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

不知道为什么,ORM(对象关系映射)给从这个类继承的类起了一个和这个类一样的名。也就是说,每当它试图为一个类创建表时,都会叫这个表'模型'。

解决办法是明确给子表命名,使用'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)
20

这个特定的错误是因为你对同一张表描述了多次。比如说,你可能在交互式解释器中重复定义了类的映射,或者在一个可以多次调用的函数里做了重复定义。对于这种情况,你需要去掉重复的调用;如果是在交互式环境中,就重新启动一个解释器,或者去掉多余的函数调用(这时候可以考虑使用单例模式)。

如果是第二种情况,也就是你把声明式的类映射和表反射混在一起了,那么你只需要按照错误提示的做法,在你的类定义中添加 __table_args__ = {'extend_existing': True} 作为一个额外的类变量。只有在你确定表确实被描述了两次的情况下,才需要这样做,特别是涉及到表反射的时候。

撰写回答