SQLAlchemy 通过关联对象声明式多对多自连接

发布于 2024-12-02 22:04:46 字数 1305 浏览 1 评论 0原文

我有一个用户表和一个朋友表,它将用户映射到其他用户,因为每个用户可以有很多朋友。这个关系显然是对称的:如果用户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.

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

I have a table Users and a table Friends which maps users to other users as each user can have many friends. This relation is obviously symmetric: if user A is a friend of user B then user B is also a friend of user A, I only store this relation once. The Friends table has additional fields besides the two User ID's so I have to use an association object.

I am trying to define this relationship in declarative style in the Users class (which extends the declarative base), but I can't seem to figure out how to do this. I want to be able to access all friends of a given user via a property friends, so say friends = bob.friends.

What's the best approach for this problem? I tried to many different setups to post here, and none of them worked for various reasons.

EDIT: My latest attempt looks like this:

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)

This however results in the following error:

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

I must admit that at this point I am thoroughly confused because of many failed attempts and might have made more than one stupid mistake in the above.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

划一舟意中人 2024-12-09 22:04:46

该特定异常是由于多次描述表而引起的,或者通过重复定义类映射(例如,在交互式解释器中,或者在可以多次调用的函数中),或者通过将声明式样式类映射与表混合反射。对于前一种情况,消除重复调用;如果您要交互执行,请启动一个新的解释器,或者消除额外的函数调用(对于单例/borg 对象来说可能是一个很好的用途)。

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

That particular exception is caused by describing the table more than once, either by repeatedly defining the class mapping (say, in the interactive interpreter, or in a function that can be called more than once), or by mixing declarative style class mappings with table reflection. In the former case, eliminate the repeated call; start a new interpreter if you are doing it interactively, or eliminate the extra function calls (possibly a good use for a singleton/borg object).

In the latter case, just do what the exception says, add __table_args__ = {'extend_existing': True} as an extra class variable in your class definitions. Only do this if you are actually sure that the table is being correctly described twice, as with table reflection.

深居我梦 2024-12-09 22:04:46

我使用 Flask-SQLAlchemy 时遇到此错误,但其他解决方案不起作用。

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

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

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)

I had this error using Flask-SQLAlchemy, but the other solutions didn't work.

The error only occurred on our production server, whilst everything ran fine on my computer and on the test server.

I had a 'Model' class that all my other database classes inherited from:

class Model(db.Model):

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

For some reason, the ORM gave classes that inherited from this class the same table name as this class. That is, for every class it tried to make a table for it called the table 'model'.

The solution was to explicitly name child-tables with the 'tablename' class variable:

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)
梦罢 2024-12-09 22:04:46

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

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

As mentioned in the comment, I prefer the extended model where the Friendship is an entity on its own and the links between friends are yet separate entities. In this way one can store the properties which are symmetrial as well as assymetrical ones (like what one person thinks about the other). As such the model below should show you what I mean:

...
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

In this way, adding a friendship is pretty easy.
So is updating any attributes of it. You can create more helper methods like add_friend.
With the cascade configuration above also deleting a User or Friendship or UserFriend will make sure that both sides are deleted.
Selecting all friends is as straighforward as you want: print user.friends

The real problem with this solution is to ensure that there are exactly 2 UserFriend links for each Friendship. Again, when manipulating the objects from the code it should not be a problem, but database might potentially be inconsistent if someone imports/manipulates some data directly in the SQL side.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文