无法使用 pre_save 信号在 django 模型中保存相关对象

Posted

技术标签:

【中文标题】无法使用 pre_save 信号在 django 模型中保存相关对象【英文标题】:Can't save related objects in django models using pre_save signal 【发布时间】:2018-05-22 20:29:36 【问题描述】:

我必须在 Django ORM 中实现 multi-aspect type of inheritance from UML。我有Contract 数据类型,根据客户类型(普通客户或商业客户)可以分类为RegularContractBusinessContract。合同也可以有到期日或不可到期(未指定有效期多长时间),因此它也可以是ExpiringContractNonExpiringContract 类型。这是概念图的外观:

这就是我的实现方式:

models.py 代码:

class Contract(models.Model):
    approval_date = models.DateTimeField(null=False)

    def __getattr__(self, item):
        if self.expiringcontract:
            return getattr(self.expiringcontract, item)
        elif self.nonexpiringcontract:
            return getattr(self.nonexpiringcontract, item)


class ContractExpirationExtension(models.Model):
    base = models.OneToOneField("website.Contract",
                                on_delete=models.CASCADE)

    class Meta:
        abstract = True


class ExpiringContract(ContractExpirationExtension):
    termination_date = models.DateTimeField()

    @property
    def duration(self):
        return self.termination_date - self.base.approval_date


class NonExpiringContract(ContractExpirationExtension):

    @property
    def duration(self):
        return timedelta(days=100)


class ContractTypeExtension(models.Model):

    base = models.OneToOneField("website.Contract", on_delete=models.CASCADE)
    termination_delay = models.PositiveSmallIntegerField(default=30)

    class Meta:
        abstract = True

    @classmethod
    def create(cls, approval_date, contract_expiration_type, termination_delay, **kwargs):
        type_extension = cls(termination_delay=termination_delay)
        base = Contract(approval_date=approval_date)
        expiration_type = contract_expiration_type(**kwargs)
        expiration_type.base = base
        type_extension.base = base
        if contract_expiration_type.__name__ == ExpiringContract.__name__:
            type_extension.base.expiringcontract = expiration_type
        elif contract_expiration_type.__name__ == NonExpiringContract.__name__:
            type_extension.base.nonexpiringcontract = expiration_type
        return type_extension

    def __getattr__(self, item):
        if self.base:
            return getattr(self.base,item)


class RegularContract(ContractTypeExtension):
    termination_delay = models.PositiveSmallIntegerField(validators=[validate_term_delay_regular], blank=False)


class BusinessContract(ContractTypeExtension):
    termination_delay = models.PositiveSmallIntegerField(validators=[validate_term_delay_business], blank=False)

当我们需要创建新的合约模型实例时,我们使用继承 ContractTypeExtension 抽象类的类中的 create() 方法。在create() 方法中,我根据传递给create() 方法的类对象参数创建Contract 基本实例和适当的到期或非到期合同实例:

@classmethod
def create(cls, approval_date, contract_expiration_type, termination_delay, **kwargs):
    type_extension = cls(termination_delay=termination_delay)
    base = Contract(approval_date=approval_date)
    expiration_type = contract_expiration_type(**kwargs)
    expiration_type.base = base
    type_extension.base = base
    if contract_expiration_type.__name__ == ExpiringContract.__name__:
        type_extension.base.expiringcontract = expiration_type
    elif contract_expiration_type.__name__ == NonExpiringContract.__name__:
        type_extension.base.nonexpiringcontract = expiration_type
    return type_extension

因为我的常规或业务合同实例中包含其他模型实例,所以如果不先保存 baseexpiration_type 实例,我将无法保存它,所以我决定创建 pre_save 信号,它可以做到这一点:

signals.py

from django.db.models.signals import pre_save, pre_delete from django.dispatch import receiver

from .models import RegularContract, BusinessContract

@receiver(pre_save, sender=RegularContract) 
@receiver(pre_save, sender=BusinessContract) 
def pre_save_contract(sender, instance, *args,**kwargs):
    print("Pre_save")
    if not instance.id:
        instance.base.save()
        try:
            instance.base.expiringcontract.save()
        except (TypeError, ValueError):
            instance.base.nonexpiringcontract.save()

我在应用程序的__init__apps.py 配置中注册了我的信号文件:

apps.py

from django.apps import AppConfig


class WebsiteConfig(AppConfig):
    name = 'website'

    def ready(self):
       import website.signals

website.__init__.py

default_app_config = 'website.apps.WebsiteConfig'

为了测试我的代码,我编写了简单的测试用例:

class BusinessContractTestCase(TestCase):

    def setUp(self):
        pass

    def test_exprirating_creation(self):
        approval_date = datetime.today()
        termination_delay = 30
        termination_date = approval_date+timedelta(days=720)
        contract = BusinessContract.create(approval_date=approval_date,                                                         contract_expiration_type=ExpiringContract,
                                      termination_delay=termination_delay,
                                       termination_date=termination_date)
        contract.save()
        self.assertEqual(contract.termination_date.date(), ExpiringContract.objects.first().termination_date.date())


class RegularContractTestCase(TestCase):

    def test_exprirating_creation(self):
        approval_date = datetime.today()
        termination_delay = 30
        termination_date = approval_date + timedelta(days=720)
        contract = RegularContract.create(approval_date=approval_date,
                                      contract_expiration_type=ExpiringContract,
                                      termination_delay=termination_delay,
                                      termination_date=termination_date)
        contract.save()
        self.assertEqual(contract.termination_date.date(),
                           ExpiringContract.objects.first().termination_date.date())

但是当尝试运行这个测试时,它们失败了,我得到了这个错误:

Error
Traceback (most recent call last):
File "/home/ubuntu/workspace/webapp/website/tests.py", line 21, in test_exprirating_creation
contract.save()
File "/home/ubuntu/workspace/venv/lib/python3.5/site-packages/django/db/models/base.py", line 685, in save
"unsaved related object '%s'." % field.name
ValueError: save() prohibited to prevent data loss due to unsaved related object 'base'.

那么为什么我的代码中没有触发pre_save 信号?

【问题讨论】:

pre_save 级别,对象还没有主键,所以这有点“鸡和蛋”的问题:你想保存一个对象链接到尚未存储在数据库中的对象。通常在这种情况下最好执行post_save 【参考方案1】:

经过短暂的调试,我明白了我的问题(感谢Willem Van Onsem 用pre_save 指出了这个细节。)这就是我解决它的方法。我稍微修改了create() 方法。我没有将base 直接分配给新创建的实例并将expiration_type 分配给基数,而是将它们保存到临时变量中,以便以后在我的信号方法中使用:

@classmethod
def create(cls, approval_date, contract_expiration_type, termination_delay, **kwargs):
    type_extension = cls(termination_delay=termination_delay)
    base = Contract(approval_date=approval_date)
    expiration_type = contract_expiration_type(**kwargs)
    type_extension.temp_base = base
    if contract_expiration_type.__name__ == ExpiringContract.__name__:
        type_extension.temp_expiringcontract = expiration_type
    elif contract_expiration_type.__name__ == NonExpiringContract.__name__:
        type_extension.base.temp_nonexpiringcontract = expiration_type
    return type_extension

然后在signals.pypre_save信号中,我分别从临时变量中保存基并分配给实例的基,并将我的到期/非到期合同类型实例从临时变量分别分配给基并保存:

@receiver(pre_save, sender=RegularContract)
@receiver(pre_save, sender=BusinessContract)
def pre_save_contract(sender, instance, *args, **kwargs):
    print("Pre_save")
    instance.temp_base.save()
    instance.base = instance.temp_base
    if hasattr(instance,"temp_expiringcontract"):
        instance.base.expiringcontract = instance.temp_expiringcontract
        instance.base.expiringcontract.save()
    else:
        instance.base.nonexpiringcontract =    instance.temp_nonexpiringcontract
        instance.base.nonexpiringcontract.save()

这可能不是最好的解决方案,但至少它有效。

【讨论】:

我觉得这个方案其实还不错。我在预保存信号中遇到了类似的地理编码问题,并且需要创建与地理编码对象的位置关系(尽管对象尚未保存)。我可以将 post_save 信号拾取的临时变量存储为解决方案。

以上是关于无法使用 pre_save 信号在 django 模型中保存相关对象的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Django 信号(Pre_save,Post_save)从“B”模型的 ImageField 设置“A”模型的 ImageField

将pre_save信号更改为post_save?Django

Django:信号的使用

Django signal 信号机制的使用

Django信号

在Django中获取当前用户登录信号