SQLAlchemy 通过关联对象声明性多对多自联接
Posted
技术标签:
【中文标题】SQLAlchemy 通过关联对象声明性多对多自联接【英文标题】:SQLAlchemy declarative many-to-many self-join via Association object 【发布时间】:2011-11-10 16:43:24 【问题描述】:我有一个表 Users 和一个表 Friends 将用户映射到其他用户,因为每个用户可以有很多朋友。这个关系显然是对称的:如果用户 A 是用户 B 的朋友,那么用户 B 也是用户 A 的朋友,我只存储这个关系一次。 Friends 表除了两个用户 ID 之外还有其他字段,因此我必须使用关联对象。
我试图在 Users 类中以声明式的方式定义这种关系(它扩展了声明式基础),但我似乎无法弄清楚如何做到这一点。我希望能够通过属性friends访问给定用户的所有朋友,所以说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.
我必须承认,在这一点上,由于许多失败的尝试,我完全糊涂了,并且可能在上面犯了不止一个愚蠢的错误。
【问题讨论】:
附加字段是什么?我想知道他们是否对两个朋友都很常见,或者是directional
。
看另一个关于 SO [***.com/questions/7120774/…: 它具有相似的结构,Gary 试图通过使用视图来解决对称性。我个人不是这个想法的忠实拥护者,但您可能想尝试一下。
附加字段是一个状态字段,它指示 1) 好友请求是否已发送或 b) 已发送并接受。将来我可能需要更多领域。加里的方法确实有点难看,我非常想要一个更好的解决方案。有人吗?
"请求已被接受?"被谁接受,或者朋友?对我来说,这听起来不像是一种对称的关系。您可能想要排除额外的字段,以便朋友关系确实是对称的,或者重新考虑对称要求并且只有两行,“a 是 b 的朋友,b 是 a 的朋友”,以便您可以捕获不对称更优雅。
这些是您定义的唯一模型,还是您在项目的其他地方定义了其他模型?
【参考方案1】:
该特定异常是由多次描述表引起的,或者通过重复定义类映射(例如,在交互式解释器中,或者在可以多次调用的函数中),或者通过混合声明式样式类具有表反射的映射。前一种情况,消除重复调用;如果您以交互方式执行它,请启动一个新的解释器,或者消除额外的函数调用(可能对单例/borg 对象很有用)。
在后一种情况下,只需按照异常的说明进行操作,在类定义中添加 __table_args__ = 'extend_existing': True
作为额外的类变量。仅当您确实确定表被正确描述了两次时才执行此操作,就像表反射一样。
【讨论】:
我在一个名为 User.py 的文件中定义了两个类/类映射,并从我需要使用这些模型的任何地方导入它。这是错的吗?我检查并仔细检查了代码中的导入周期,但找不到任何... 在类定义之前放一个print
语句,看看它是否被多次导入。如果是,请在调试模式下运行或打印 stacktrace 以查看为什么模块被加载/导入两次
要详细说明van的建议,加print "models imported as", __name__
谢谢。我需要在解释器中重新定义一个类,但我意外地查找错误找不到如何执行此操作的示例。【参考方案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)
【讨论】:
【参考方案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
配置,还删除 User or Friendship or UserFriend
将确保双方都被删除。选择所有朋友都随心所欲:@ 987654326@
此解决方案的真正问题是确保每个Friendship
恰好有2 个UserFriend
链接。同样,在从代码中操作对象时,这应该不是问题,但如果有人直接在 SQL 端导入/操作某些数据,数据库可能会出现不一致。
【讨论】:
以上是关于SQLAlchemy 通过关联对象声明性多对多自联接的主要内容,如果未能解决你的问题,请参考以下文章
Flask/SQLAlchemy - 多对多关系的关联模型和关联表之间的区别?