重写Django模型save方法触发唯一约束错误的解决方法
Django IntoDocumentProduct模型save方法重写导致主键重复的解决办法
问题情况
重写IntoDocumentProduct模型的save方法时,想在创建新对象后基于关联外键赋值,于是在self.pk为None(新对象)时先调用父类的save生成主键,之后再调用一次父类save保存逻辑结果,结果触发IntegrityError,提示主键重复。直接用self.save()又会导致递归循环。
报错信息
API测试时的错误
IntegrityError at /api/wms/intodocuments/products/create/ duplicate key value violates unique constraint "wms_dokumentprzyjeciaprodukt_pkey" DETAIL: Key (id)=(151) already exists.
控制台回溯
Traceback (most recent call last): File "W:\projects\foodgast\venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner response = get_response(request) ^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view return view_func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\views\generic\base.py", line 103, in view return self.dispatch(request, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\rest_framework\views.py", line 509, in dispatch response = self.handle_exception(exc) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\rest_framework\views.py", line 469, in handle_exception self.raise_uncaught_exception(exc) File "W:\projects\foodgast\venv\Lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception raise exc File "W:\projects\foodgast\venv\Lib\site-packages\rest_framework\views.py", line 506, in dispatch response = handler(request, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Sebastian\AppData\Local\Programs\Python\Python311\Lib\contextlib.py", line 81, in inner return func(*args, **kwds) ^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\src\wms\api\views.py", line 183, in post serializer.save() File "W:\projects\foodgast\venv\Lib\site-packages\rest_framework\serializers.py", line 212, in save self.instance = self.create(validated_data) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\rest_framework\serializers.py", line 962, in create instance = ModelClass._default_manager.create(**validated_data) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\models\manager.py", line 85, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\models\query.py", line 671, in create obj.save(force_insert=True, using=self.db) File "W:\projects\foodgast\src\wms\models.py", line 621, in save super(IntoDocumentProduct, self).save(*args, **kwargs) File "W:\projects\foodgast\venv\Lib\site-packages\django\db\models\base.py", line 812, in save self.save_base( File "W:\projects\foodgast\venv\Lib\site-packages\django\db\models\base.py", line 863, in save_base updated = self._save_table( ^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\models\base.py", line 1006, in _save_table results = self._do_insert( ^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\models\base.py", line 1047, in _do_insert return manager._insert( ^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\models\manager.py", line 85, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\models\query.py", line 1791, in _insert return query.get_compiler(using=using).execute_sql(returning_fields) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\models\sql\compiler.py", line 1660, in execute_sql cursor.execute(sql, params) File "W:\projects\foodgast\venv\Lib\site-packages\django\db\backends\utils.py", line 103, in execute return super().execute(sql, params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\backends\utils.py", line 67, in execute return self._execute_with_wrappers( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\backends\utils.py", line 80, in _execute_with_wrappers return executor(sql, params, many, context) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "W:\projects\foodgast\venv\Lib\site-packages\django\db\backends\utils.py", line 84, in _execute with self.db.wrap_database_errors: File "W:\projects\foodgast\venv\Lib\site-packages\django\db\utils.py", line 91, in __exit__ raise dj_exc_value.with_traceback(traceback) from exc_value File "W:\projects\foodgast\venv\Lib\site-packages\django\db\backends\utils.py", line 89, in _execute return self.cursor.execute(sql, params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ django.db.utils.IntegrityError: duplicate key value violates unique constraint "wms_dokumentprzyjeciaprodukt_pkey" DETAIL: Key (id)=(150) already exists.
问题根源
原代码里,创建新对象时第一次调用父类save已经生成了主键,之后第二次调用父类save时,因为create方法调用save时传入了force_insert=True,Django还是尝试执行插入操作而非更新,导致主键重复冲突。
原问题代码:
def save(self, *args, **kwargs): if self.pk is None: super(IntoDocumentProduct, self).save(*args, **kwargs) # some logic # more logic super(IntoDocumentProduct, self).save(*args, **kwargs) self.full_clean()
解决办法
办法1:强制第二次保存为更新操作
修改save方法,第一次保存生成主键后,第二次调用父类save时传入force_update=True,告诉Django执行更新而非插入:
def save(self, *args, **kwargs): if self.pk is None: # 第一次保存生成主键 super().save(*args, **kwargs) # 这里写基于外键的赋值逻辑 # 比如:self.quantity = self.into_document.default_quantity # 通用逻辑放在这里 # more logic # 第二次保存,强制更新操作 save_kwargs = kwargs.copy() if self.pk is not None: save_kwargs['force_update'] = True super().save(*args, **save_kwargs) self.full_clean()
办法2:先拿外键数据再一次性保存
如果不需要提前生成主键,可以直接从关联外键对象获取数据赋值,之后只执行一次save,避免两次保存:
def save(self, *args, **kwargs): if self.pk is None: # 直接从外键对象取数据赋值,不用提前保存 # 比如:self.price = self.product.retail_price # 通用逻辑 # more logic super().save(*args, **kwargs) self.full_clean()
办法3:用post_save信号替代重写save方法
如果必须在对象创建完成后执行逻辑,推荐用Django的post_save信号,这样不用修改save方法,也不会有递归或重复插入的问题:
from django.db.models.signals import post_save from django.dispatch import receiver from .models import IntoDocumentProduct @receiver(post_save, sender=IntoDocumentProduct) def update_product_after_create(sender, instance, created, **kwargs): if created: # 对象刚创建完成,执行逻辑 # 比如:instance.total = instance.quantity * instance.price # 只更新需要修改的字段,避免触发循环 instance.save(update_fields=['total'])
内容的提问来源于stack exchange,提问作者skelaw
相关产品推荐
相关产品推荐

