后期绑定多对多自引用关系的语法

Syntax for late-binding many-to-many self-referential relationship

我找到了很多关于如何使用单独的 table 或 class 创建自我引用的多对多关系(对于用户关注者或朋友)的解释:

以下是三个示例,其中一个来自 Mike Bayer 本人:

但在我发现的每个示例中,定义关系中 primaryjoinsecondaryjoin 的语法都是早期绑定语法:

# this relationship is used for persistence
friends = relationship("User", secondary=friendship, 
                       primaryjoin=id==friendship.c.friend_a_id,
                       secondaryjoin=id==friendship.c.friend_b_id,
)

这很好用,除了一种情况:当使用 Base class 为所有对象定义 id 列时,如 Mixins: Augmenting the base 所示来自文档

我的Baseclass和followerstable是这样定义的:

from flask_sqlchalchemy import SQLAlchemy
db = SQLAlchemy()

class Base(db.Model):
    __abstract__ = True
    id = db.Column(db.Integer, primary_key=True)

user_flrs = db.Table(
    'user_flrs',
    db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
    db.Column('followed_id', db.Integer, db.ForeignKey('user.id')))

但现在我在将 id 移动到 mixin 之前忠诚地为我服务了一段时间的追随者关系遇到了麻烦:

class User(Base):
    __table_name__ = 'user'
    followed_users = db.relationship(
        'User', secondary=user_flrs, primaryjoin=(user_flrs.c.follower_id==id),
        secondaryjoin=(user_flrs.c.followed_id==id),
        backref=db.backref('followers', lazy='dynamic'), lazy='dynamic')

db.class_mapper(User)  # trigger class mapper configuration

大概是因为 id 不在本地范围内,尽管它似乎为此引发了一个奇怪的错误:

ArgumentError: Could not locate any simple equality expressions involving locally mapped foreign key columns for primary join condition 'user_flrs.follower_id = :follower_id_1' on relationship User.followed_users. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or are annotated in the join condition with the foreign() annotation. To allow comparison operators other than '==', the relationship can be marked as viewonly=True.

如果我将括号更改为引号以利用后期绑定,它会抛出相同的错误。我不知道如何用 foreign()remote() 注释这个东西,因为我根本不知道 sqlalchemy 希望我在跨越次要 table!我已经尝试了很多这样的组合,但到目前为止还没有奏效。

我遇到了一个非常相似(虽然不完全相同)的问题,自引用关系跨越一个单独的table,关键是简单地转换后期绑定的 remote_side 参数。这对我来说很有意义,因为 id 列在早期绑定过程中不存在。

如果我遇到的问题不是后期绑定,请指教。不过,在当前范围内,我的理解是 id 映射到 Python 内置 id(),因此不会作为早期绑定关系工作。

在联接中将 id 转换为 Base.id 会导致以下错误:

ArgumentError: Could not locate any simple equality expressions involving locally mapped foreign key columns for primary join condition 'user_flrs.follower_id = "<name unknown>"' on relationship User.followed_users. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or are annotated in the join condition with the foreign() annotation. To allow comparison operators other than '==', the relationship can be marked as viewonly=True.

您不能在联接过滤器中使用 id,不,因为那是 built-in id() function,而不是 User.id 列。

您有三个选择:

  1. 在 创建您的 User 模型后定义关系 ,将其分配给新的 User 属性;然后你可以引用 User.id 因为它是从基地拉进来的:

    class User(Base):
        # ...
    
    User.followed_users = db.relationship(
        User,
        secondary=user_flrs,
        primaryjoin=user_flrs.c.follower_id == User.id,
        secondaryjoin=user_flrs.c.followed_id == User.id,
        backref=db.backref('followers', lazy='dynamic'),
        lazy='dynamic'
    )
    
  2. 使用字符串作为连接表达式。 relationship() 的任何字符串参数在配置映射器时被评估为 Python 表达式,而不仅仅是第一个参数:

    class User(Base):
        # ...
    
        followed_users = db.relationship(
            'User',
            secondary=user_flrs,
            primaryjoin="user_flrs.c.follower_id == User.id",
            secondaryjoin="user_flrs.c.followed_id == User.id",
            backref=db.backref('followers', lazy='dynamic'),
            lazy='dynamic'
        )
    
  3. 将关系定义为可调用项;这些在映射器配置时调用以生成最终对象:

    class User(Base):
        # ...
    
        followed_users = db.relationship(
            'User',
            secondary=user_flrs,
            primaryjoin=lambda: user_flrs.c.follower_id == User.id,
            secondaryjoin=lambda: user_flrs.c.followed_id == User.id,
            backref=db.backref('followers', lazy='dynamic'),
            lazy='dynamic'
        )
    

后两个选项见sqlalchemy.orgm.relationship() documentation:

Some arguments accepted by relationship() optionally accept a callable function, which when called produces the desired value. The callable is invoked by the parent Mapper at “mapper initialization” time, which happens only when mappers are first used, and is assumed to be after all mappings have been constructed. This can be used to resolve order-of-declaration and other dependency issues, such as if Child is declared below Parent in the same file*[.]*

[...]

When using the Declarative extension, the Declarative initializer allows string arguments to be passed to relationship(). These string arguments are converted into callables that evaluate the string as Python code, using the Declarative class-registry as a namespace. This allows the lookup of related classes to be automatic via their string name, and removes the need to import related classes at all into the local module space*[.]*

[...]

  • primaryjoin

    [...]

    primaryjoin may also be passed as a callable function which is evaluated at mapper initialization time, and may be passed as a Python-evaluable string when using Declarative.

[...]

  • secondaryjoin

    [...]

    secondaryjoin may also be passed as a callable function which is evaluated at mapper initialization time, and may be passed as a Python-evaluable string when using Declarative.

字符串和 lambda 都定义了与第一个选项中使用的相同的 user_flrs.c.followed_id == User.id / user_flrs.c.follower_id == User.id 表达式,但是因为它们分别作为字符串和可调用函数给出,所以您推迟计算直到 SQLAlchemy 需要完成这些声明。