django-import-export导入外键数据报get()返回多个Block错误排查
向带外键的模型/表导入数据时,部分行导入失败抛出如下错误:
Line number: 1 - get() returned more than one Block -- it returned 2! , HARYANA, 11, FATEHABAD, 195, FATEHABAD, 641, AHLISADAR, 57891 Traceback (most recent call last): File "/home/webtesting/django_venv/lib/python3.9/site-packages/import_export/resources.py", line 660, in import_row self.before_import_row(row, **kwargs) File "/home/webtesting/Excel/DAMU_Excel_Upload_AMFU_DAMU/damuwhatsapp/resources.py", line 89, in before_import_row (cat, _created) = Block.objects.get_or_create(block_name=block_name) File "/home/webtesting/django_venv/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "/home/webtesting/django_venv/lib/python3.9/site-packages/django/db/models/query.py", line 657, in get_or_create return self.get(**kwargs), False File "/home/webtesting/django_venv/lib/python3.9/site-packages/django/db/models/query.py", line 499, in get raise self.model.MultipleObjectsReturned( damuwhatsapp.models.Block.MultipleObjectsReturned: get() returned more than one Block -- it returned 2!
报错核心是Django ORM的get()方法要求查询结果必须有且仅有1条匹配记录,但当前查询匹配到了2条及以上的Block记录。
相同结构的数据部分导入成功、部分失败的逻辑非常明确:
- 导入成功的行对应的
block_name,在Block表中要么只有1条匹配记录,要么完全不存在(get_or_create会自动创建新记录) - 导入失败的行对应的
block_name,在Block表中已经存在至少2条block_name值完全相同的重复记录,直接触发MultipleObjectsReturned异常
现有代码存在两个设计缺陷放大了这个问题:
- Block模型的
block_name字段没有加唯一约束,历史数据中已经产生了重名记录,且后续新增数据时也没有机制阻止重名Block生成 - 已经通过
ForeignKeyWidget配置了外键关联逻辑,又在before_import_row钩子中冗余写了get_or_create查询逻辑,提前触发了重复数据的报错,且钩子中给row['block_name']赋值名称字符串的操作完全多余,后续ForeignKeyWidget还是会重新做一次查询。
按以下步骤操作即可彻底解决问题:
第一步:清理Block表现有重复数据
先执行脚本找出所有重名的Block记录,手动合并、删除重复项,保证每个block_name仅对应1条Block记录:from django.db.models import Count # 查询所有重名的block_name duplicate_block_names = Block.objects.values('block_name')\ .annotate(record_count=Count('id'))\ .filter(record_count__gt=1) print(duplicate_block_names)清理时注意检查是否存在值前后带空格、大小写不一致的“隐形重复”(比如
FATEHABAD和FATEHABAD视觉上无差异,但实际是不同字符串),可统一做去空格处理。第二步:给Block表加唯一约束,从根源避免重复数据
修改Block模型的block_name字段,添加unique=True参数:class Block(models.Model): # 其余原有字段保持不变 block_name = models.CharField(max_length=自定义字段长度, unique=True)保存后依次执行
python manage.py makemigrations、python manage.py migrate命令,将约束应用到数据库。第三步:简化导入逻辑,删除冗余代码
直接删除VillageResource中整个before_import_row方法,ForeignKeyWidget本身已经实现了按block_name查询关联Block实例的逻辑,不需要手动在钩子中重复查询。
如果需要实现“导入时遇到不存在的Block自动创建”的能力,自定义支持自动创建的ForeignKeyWidget替换原有组件即可,不要在导入钩子中单独写查询逻辑:from import_export.widgets import ForeignKeyWidget class AutoCreateForeignKeyWidget(ForeignKeyWidget): def clean(self, value, row=None, *args, **kwargs): # 先查询关联记录 instance = super().clean(value, row, *args, **kwargs) # 不存在则自动创建 if not instance: instance = self.model.objects.create(**{self.field: value}) return instance # 替换原有block_name字段的widget配置 block_name = fields.Field( column_name='block_name', attribute='block_name', widget=AutoCreateForeignKeyWidget(Block, 'block_name') )
内容的提问来源于stack exchange,提问作者Abhilash Singh Chauhan

