Django save() prohibited to prevent data loss due to unsaved related object

1
2
3
4
5
6
7
8
9
10
11
# create a new section
p_section = Section(name=control.get('Section', _('Default')))
p_section.save()
# create a new package
p_package = Package(
name=control.get('Name', _('Untitled Package')),
package=control.get('Package'),
section=p_section,
)
p_package.save()

在上例中,p_package.save() 这行发生了错误:

django save() prohibited to prevent data loss due to unsaved related object ‘section’

而经过调试,很明显 p_section 是 save() 过而且非 None 的,但为什么 Django 依旧报错并提示没有 save() 过呢?这个问题曾困扰博主近两个小时,完全找不到头绪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# A pk may have been assigned manually to a model instance not
# saved to the database (or auto-generated in a case like
# UUIDField), but we allow the save to proceed and rely on the
# database to raise an IntegrityError if applicable. If
# constraints aren't supported by the database, there's the
# unavoidable risk of data corruption.
if obj and obj.pk is None:
# Remove the object from a related instance cache.
if not field.remote_field.multiple:
delattr(obj, field.remote_field.get_cache_name())
raise ValueError(
"save() prohibited to prevent data loss due to "
"unsaved related object '%s'." % field.name
)

值得注意的是,Django 源码中:if obj and obj.pk is None,对 model 的主键 Primary Key 进行了检查,如果主键为空,则抛出提示 model 未 save() 过。
而这样的提示显然会误导程序员,让我们看看 Section 类的定义:

id = models.IntegerField(primary_key=True, editable=False)

这样做看似没有问题,Section 类成员的增删查改都完全正常,但是如果 Section 在创建时就作为外键赋值,会找不到主键。

If you don’t use AutoField, Django doesn’t know that it needs to update the field after saving.
It assumes that you will always provide a value for the primary key, and if you don’t, that you’ll receive an OperationError from the DB-API. Because you’ve specified AUTO_INCREMENT in the database (or SQLite does that anyway), you aren’t getting that error - the database is filling in a value behind Django’s back.
Effectively this is just a mismatch between your database schema and your model definition.

这是由于程序员对于 Django 模型设计与数据库实际情况不一致所导致的,如果不将字段指定为 Auto Increment,Django 是无法得知新插入的成员主键 id 是多少的。作为 Django 程序员,设计 ORM 模型时应特别注意模型与数据库建表之间的关系,而不能盲目设计。正确的解决方法是,将该字段由 IntegerField 改为 AutoField:

id = models.AutoField(primary_key=True, editable=False)

或者,在创建 Section 时指定主键的值:

p_section.id = 1