在 Django CRM 系统中,直接删除被估计单、服务报告或发票引用的库存项会导致外键关联断裂,引发页面加载失败;正确做法是通过配置 ForeignKey.on_delete 行为(如 SET_NULL 或 PROTECT),使历史数据保持完整且可读。
在 django crm 系统中,直接删除被估计单、服务报告或发票引用的库存项会导致外键关联断裂,引发页面加载失败;正确做法是通过配置 `foreignkey.on_delete` 行为(如 `set_null` 或 `protect`),使历史数据保持完整且可读。
在典型的 Django Web 应用中,库存项(InventoryItem)常被其他模型(如 Estimate、ServiceReport、Invoice)通过 ForeignKey 引用。默认使用 models.CASCADE 时,一旦删除某库存项,所有关联报表也会被级联删除——这显然不符合业务需求;更糟的是,若误用 DO_NOTHING 且数据库未设约束,还会导致 IntegrityError。
✅ 推荐解决方案:改用 SET_NULL
该策略保留历史报表完整性,仅将已删除库存项的外键字段置为 NULL,同时在前端做优雅降级显示(例如显示“[已下架]”或“库存项不可用”):
# models.py
from django.db import models
class InventoryItem(models.Model):
name = models.CharField(max_length=100)
sku = models.CharField(max_length=50, unique=True)
# ... 其他字段
class EstimateItem(models.Model): # 假设 estimate 包含明细行
estimate = models.ForeignKey('Estimate', on_delete=models.CASCADE)
inventory_item = models.ForeignKey(
InventoryItem,
on_delete=models.SET_NULL, # ← 关键修改
null=True,
blank=True,
related_name='estimate_items'
)
quantity = models.PositiveIntegerField()
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
⚠️ 实施前提与注意事项:
- 必须将对应外键字段声明为 null=True(数据库层面允许 NULL);
- 迁移前需运行 python manage.py makemigrations 并检查生成的 SQL,确认 ALTER TABLE ... ALTER COLUMN ... DROP NOT NULL 已包含;
- 在视图或模板中主动处理 inventory_item is None 场景,避免 .name 调用引发 AttributeError:
{% for item in estimate.items.all %}
{% if item.inventory_item %}
{{ item.inventory_item.name }}
{% else %}
[已归档库存项]
{% endif %}
{{ item.quantity }} × {{ item.unit_price }}
{% endfor %}
? 其他策略对比:
YouArt
YouArt是个一站式AI图像与视...
- PROTECT:适合强一致性场景(如禁止删除任何被引用的库存),但需先手动解除关联;
- SET_DEFAULT:适用于有明确“未知商品”默认值的场景(需提前定义 default=...);
- RESTRICT:比 PROTECT 更灵活,支持跨级联删除,但复杂度高,一般不推荐用于此场景。
? 总结:SET_NULL 是平衡数据完整性与业务灵活性的最佳实践。它让库存管理具备“逻辑删除”能力——真实记录不丢失,历史报表始终可访问,同时为后续审计、归档或数据迁移提供清晰语义。切勿依赖 DO_NOTHING,它绕过 Django ORM 层校验,极易引发底层数据库异常。
就爱读