簡體   English   中英

無法使用pre_save信號在django模型中保存相關對象

[英]Can't save related objects in django models using pre_save signal

我必須在Django ORM中從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()方法中,我創建了Contract基礎實例和基於類對象參數的合適的到期或非到期合同實例,我傳遞給create()方法:

@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()

我在app的__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信號呢?

經過簡短的調試,我已經理解了我的問題(感謝Willem Van Onsempre_save指出了這個細節。)這就是我解決它的方法。 我略微修改了create()方法。 不是將base直接分配給新創建的實例,而是將expiration_type分配給base ,而是將它們保存到臨時變量中,以后我可以在我的signal方法中使用它們:

@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信號我單獨保存的臨時變量的基礎,並分配給實例的基地,並分別指派我的到期/從臨時變量nonexpiring合同類型的實例立足並保存:

@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()

這可能不是最好的解決方案,但至少它可行。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM