SQLAlchemy通过Association对象声明多对多自联接

时间:2011-09-05 14:44:06

标签: python sqlalchemy many-to-many relationship declarative

我有一个表用户和一个表朋友将用户映射到其他用户,因为每个用户可以有很多朋友。这种关系显然是对称的:如果用户A是用户B的朋友,那么用户B也是用户A的朋友,我只存储一次这种关系。除了两个用户ID之外,Friends表还有其他字段,因此我必须使用关联对象。

我试图在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.

我必须承认,在这一点上,由于许多失败的尝试,我完全感到困惑,可能在上面犯了不止一个愚蠢的错误。

3 个答案:

答案 0 :(得分:20)

通过重复定义类映射(例如,在交互式解释器中,或在可以多次调用的函数中)或通过混合声明式样式类,不止一次地描述表来引起该特定异常与表反射的映射。在前一种情况下,消除重复呼叫;如果你以交互方式进行,或者消除额外的函数调用(可能很好地用于singleton / borg对象),就启动一个新的解释器。

在后一种情况下,只需执行异常所说的内容,在类定义中添加__table_args__ = {'extend_existing': True}作为额外的类变量。只有在您确实正确地将表格正确描述两次时才执行此操作,如表格反射。

答案 1 :(得分:1)

我使用Flask-SQLAlchemy时出现此错误,但其他解决方案无效。

错误只发生在我们的生产服务器上,而我的计算机和测试服务器上的一切运行正常。

我有一个'Model'类,我的所有其他数据库类都继承自:

class Model(db.Model):

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

出于某种原因,ORM为从此类继承的类提供了与此类相同的名称。也就是说,对于每个类,它试图为它创建一个表,称为表'model'。

解决方案是使用' 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)

答案 2 :(得分:0)

正如评论中所提到的,我更喜欢扩展模型,其中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

此解决方案的真正问题是确保每个UserFriend只有2 Friendship个链接。同样,当从代码中操作对象时,它应该不是问题,但如果有人直接在SQL端导入/操作某些数据,则数据库可能会不一致。