Django + PostgreSQL触发器导致“违反外键约束”

3
在我的Django...模式中,我有一个产品模型结构,其中包括产品价格以及产品历史价格记录(类似于日志)。
有点像以下内容(为了这个问题而简化):
class ProductPricing(models.Model):
    price = models.DecimalField(max_digits=8, decimal_places=2,
                                help_text='price in dollars')
    product = models.OneToOneField('app.Product', related_name='pricing',
                                    null=False, on_delete=models.CASCADE)

class Product(models.Model):
    name = models.CharField(max_length=100)
    # .pricing --> related name from 'ProductPricing'

为了保留价格历史记录,我有一个名为OldProductPricing的模型,它与ProductPricing基本相同,但允许每个产品存在多个OldProductPricing
class OldProductPricing(models.Model):
    valid_until = models.DateTimeField(null=False)
    product = models.ForeignKey('app.Product', related_name='+',
                                null=False, on_delete=models.CASCADE)

为了确保每次修改或删除产品价格时,我会尝试在我的OldProductPricing表中创建一个条目。
为此,我已经在Posgresql上创建了一个触发器,用于更新或删除操作:
CREATE TRIGGER price_change_trigger AFTER UPDATE OR DELETE ON app.productpricing
FOR EACH ROW EXECUTE PROCEDURE price_change_trigger_func();

触发器函数创建插入部分的代码如下:
CREATE OR REPLACE FUNCTION price_change_trigger_func() RETURNS TRIGGER LANGUAGE plpgsql AS $$
    DECLARE
       retval RECORD;
    BEGIN
        IF (TG_OP = 'DELETE') THEN
            retval := OLD;
        ELSE
            retval := NEW;
        END IF;

        RAISE LOG 'Inserting in old_prices table copy of Pricing record ID=%% for product_id=%% because of %%', OLD.id, OLD.product_id, TG_OP;
        old_to_insert.product_id := OLD.product_id;
        old_to_insert.price := OLD.price;
        old_to_insert.valid_until := now() at time zone 'utc';
        old_to_insert.id := nextval('app_oldproductpricing_id_seq');

        IF EXISTS(SELECT 1 FROM app_product WHERE app_product.id=old_to_insert.product_id)) THEN
            INSERT INTO app_oldproductpricing VALUES(old_to_insert.*);
        END IF;
        RETURN retval;
    EXCEPTION WHEN others THEN
        -- Do nothing...
        RAISE WARNING 'Got error on %% %% %%', TG_OP, SQLERRM, SQLSTATE;
        RETURN NULL;
    END;

大多数情况下,它工作得很好,但当删除Product并且开始发生CASCADES(从Product -到-> ProductPricing),EXISTS(SELECT 1 FROM app_product WHERE app_product.id=old_to_insert.product_id))似乎没有效果:尽管我检查了Product是否存在,但我仍然会收到:

insert or update on table "app_oldproductpricing" violates foreign key constraint "app_oldproductpricing_product_id_bb078367_fk_app_product_id"
DETAIL:  Key (product_id)=(5) is not present in table "app_product".

我认为发生的情况是触发器在一个事务中运行,其中要删除产品 -> 产品定价,并且 EXISTS 检查发生在实际删除 Product 之前 ?? (我不是很确定,这只是一个怀疑),因此创建了一个新的 OldProductPricing 实例,但在此之后,OldProductPricing 行所引用的 Product 被删除,导致数据库处于不一致状态。

我的理论正确吗? 最重要的是...有没有办法解决这个问题?

谢谢您的帮助。任何提示或建议都将不胜感激。

PS:我使用 Django 1.11 和 Postgresql 9.5。

1个回答

4
这个问题是由于“可变表”引起的,当DJANGO模拟ON_CASCADE时,首先删除“ProductPricing”表中的行,然后插入到“OldProductPricing”表中的行超过了将要消失的“Product”表中的一行。你有其他选项:
1. 删除触发器并在Product.save()和ProductPricing.save()中管理逻辑,并将on_delete=CASCADE更改为on_delete=DO_NOTHING。
或者
2. 一个日志表(“OldProductPricing”)应该永久保留数据,并显示从“Product”中的所有更改的历史记录,因此您应该从“OldProductPricing”到“Product”的外键中删除它。

天啊...我不知道on_delete=CASCADE没有使用数据库的级联能力...原来是通过 Python实现的??? 太可怕了...没错,通过 Postgres 强制级联解决了问题。感谢你指导我正确的方向。 - Savir

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接