【问题标题】:Flask-SQLAlchemy: CircularDependencyError where same row in many-to-one relationship can be in one-to-many relationship with same tableFlask-SQLAlchemy:CircularDependencyError,其中多对一关系中的同一行可以与同一张表存在一对多关系
【发布时间】:2016-12-26 00:08:10
【问题描述】:

我正在使用 Flask-SQLAlchemy 2.1 和 SQLAlchemy 1.0.13,我有两个表,AddressCustomer,它们之间有多个关系,如下所示:

class Address(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    ...  # Other rows including first_name, last_name, etc.
    customer_id = db.Column(
        db.Integer,
        db.ForeignKey('customers.id')
    )
    customer = db.relationship(
        'Customer',
        foreign_keys=customer_id,
        back_populates='addresses'
    )

class Customer(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    billing_address_id = db.Column(db.Integer, db.ForeignKey('addresses.id'))
    billing_address = db.relationship(
        'Address',
        foreign_keys=billing_address_id
    )
    shipping_address_id = db.Column(
        db.Integer,
        db.ForeignKey('addresses.id')
    )
    shipping_address = db.relationship(
        'Address',
        foreign_keys=shipping_address_id
    )
    addresses = db.relationship(
        'Address',
        foreign_keys='Address.customer_id',
        back_populates='customer'
    )

对于Customer 实例,还有两个事件侦听器会自动将任何集合billing_addressshipping_address 添加到addresses

@event.listens_for(Customer.billing_address, 'set')
def add_billing_address_event(target, value, oldvalue, initiator):
    """If a billing address is added to a `Customer`, add it to addresses."""
    if value is not None and value not in target.addresses:
        target.addresses.append(value)


@event.listens_for(Customer.shipping_address, 'set')
def add_shipping_address_event(target, value, oldvalue, initiator):
    """If a shipping address is added to `Customer`, add to addresses."""
    if value is not None and value not in target.addresses:
        target.addresses.append(value)

尝试设置 Customer.billing_addressCustomer.shipping_address 会导致 CircularDependencyError 如我所料:

> c = Customer()
> c.billing_address = Address(first_name='Bill')
> c.shipping_address = Address(first_name='Ship')
> db.session.add(c)
> db.session.flush()

CircularDependencyError: Circular dependency detected. (ProcessState(ManyToOneDP(Customer.shipping_address), <Customer at 0x7f53aa5c9fd0>, delete=False), ProcessState(ManyToOneDP(Address.customer), <Address at 0x7f53aa4e4128>, delete=False), ProcessState(ManyToOneDP(Address.customer), <Address at 0x7f53aa4e4080>, delete=False), SaveUpdateState(<Customer at 0x7f53aa5c9fd0>), ProcessState(ManyToOneDP(Customer.billing_address), <Customer at 0x7f53aa5c9fd0>, delete=False), ProcessState(OneToManyDP(Customer.addresses), <Customer at 0x7f53aa5c9fd0>, delete=False), SaveUpdateState(<Address at 0x7f53aa4e4080>), SaveUpdateState(<Address at 0x7f53aa4e4128>))

如果我注释掉事件监听器,这不会导致CircularDependencyError,这也是我所期望的,因为Customer.address 未被访问。然而,这不是一个解决方案,因为循环依赖是由于在billing_addressshipping_addressaddresses 中存在相同的Address 实例,我想允许addresses 包括当前的计费和运输地址。

根据relevant SQLAlchemy docs,这应该可以通过在关系的一侧添加post_update=True 参数并为其外键命名来解决:

class Address(db.Model):
    ...
    customer_id = db.Column(
        db.Integer,
        db.ForeignKey('customers.id', name='fk_customer_id')
    )
    customer = db.relationship(
        'Customer',
        foreign_keys=customer_id,
        back_populates='addresses',
        post_update=True
    )

这仍然会引发CircularDependencyError,但是:

CircularDependencyError: Circular dependency detected. (ProcessState(OneToManyDP(Customer.addresses), <Customer at 0x7f620af3ff60>, delete=False), SaveUpdateState(<Address at 0x7f620ae5a080>), SaveUpdateState(<Address at 0x7f620ae5a128>), ProcessState(ManyToOneDP(Customer.billing_address), <Customer at 0x7f620af3ff60>, delete=False), SaveUpdateState(<Customer at 0x7f620af3ff60>), ProcessState(ManyToOneDP(Customer.shipping_address), <Customer at 0x7f620af3ff60>, delete=False))

我还尝试将 use_alter=True 传递给 customer_id 外键,正如一些相关 StackOverflow 帖子中提到的那样:

customer_id = db.Column(
    db.Integer,
    db.ForeignKey('customers.id', name='fk_customer_id', use_alter=True)
)

CircularDependencyError 仍然会发生。我找到了一个似乎可行的解决方案,我将在下面发布,但我不确定它是否是正确的解决方案。

【问题讨论】:

    标签: python sqlalchemy relationship flask-sqlalchemy circular-dependency


    【解决方案1】:

    在关系两边设置post_update=True似乎可以解决问题:

    class Address(db.Model):
        ...
        customer_id = db.Column(db.Integer, db.ForeignKey('customers.id'))
        customer = db.relationship(
            'Customer',
            foreign_keys=customer_id,
            back_populates='addresses',
            post_update=True
        )
    
    class Customer(db.Model):
        ...
        addresses = db.relationship(
            'Address',
            foreign_keys='Address.customer_id',
            back_populates='customer',
            post_update=True
        )
    

    现在当添加 billing_address 和/或 shipping_address 时,它会自动添加到 addresses 而不会出现问题。添加新的billing_addressshipping_address 的行为也符合我的预期,将旧地址保留在addresses 中并添加新地址。

    但是,我对这个答案并不完全有信心,因为 SQLAlchemy 文档明确提到应该为关系的一侧设置post_update=True,而不是两者,所以我想知道我的解决方案是否会导致意外行为。

    编辑 - 这是正确的解决方案:

    由于某种原因,在 addresses 上设置 post_update=True 而不同时在 customer 上设置(反之亦然)不起作用,但按照 @univerio 的建议将其设置在 billing_addressshipping_address 上。谢谢!

    class Customer(db.Model):
    ...
        billing_address = db.relationship(
            'Address',
            foreign_keys=billing_address_id,
            post_update=True
        )
        shipping_address = db.relationship(
            'Address',
            foreign_keys=shipping_address_id,
            post_update=True
        )
    

    【讨论】:

    • "标志应该只放在其中一个关系上",这是指在addresses 关系上设置post_update=True,或者在shipping_addressbilling_address 关系上设置post_update=True,不是同一addresses/customer 关系的两侧。
    猜你喜欢
    • 1970-01-01
    • 2015-11-12
    • 2019-10-15
    • 2013-02-17
    • 1970-01-01
    • 1970-01-01
    • 2012-08-27
    • 2020-09-09
    • 1970-01-01
    相关资源
    最近更新 更多