如何用django-import-export导入含关联Activity的Project数据?
解决django-import-export导入带关联Activity的Project数据问题
问题背景
现有两个Django模型Project和Activity,Activity通过外键关联Project。已实现将每个Project的所有Activity以/分隔导出到单个单元格,但需要反向实现从xlsx文件导入包含Activity的Project数据,核心需求是先保存Project获取ID,再解析单元格中的Activity信息并创建关联记录。
模型代码
from django.db import models from datetime import date class Project(models.Model): reg_date = models.DateField(default=date.today) name = models.CharField(max_length=100, unique=True) def __str__(self): return self.name class Activity(models.Model): schedule = models.ForeignKey(Project, on_delete=models.CASCADE) date = models.DateField() description = models.TextField(max_length=1000) class Meta: unique_together = ('schedule', 'date', 'description')
完整的ProjectResource实现
需要重写after_save_instance方法(在Project实例保存后执行),同时修正导出时的反向关联查询,确保导出和导入的格式一致:
from import_export import resources, fields from import_export.widgets import DateWidget from .models import Project, Activity from datetime import datetime class ProjectResource(resources.ModelResource): activities = fields.Field() class Meta: model = Project import_id_fields = ['name'] exclude = ('id',) skip_unchanged = True report_skipped = True fields = ('reg_date', 'name', 'activities') widgets = { 'reg_date': DateWidget(format='%Y-%m-%d'), } # 修正导出逻辑:正确获取关联的Activity集合 def dehydrate_activities(self, obj): if obj.id: # 按导出示例格式拼接:日期-描述,用/分隔 return "/".join([ f"{activity.date}-{activity.description}" for activity in obj.activity_set.all() ]) return "" # 跳过已存在的Project(根据name判断) def skip_row(self, instance, original, row, import_validation_errors=None): return original.name is not None # 导入时处理关联的Activity def after_save_instance(self, instance, using_transactions, dry_run): # dry_run为True时仅验证不实际保存,跳过处理 if dry_run: return # 获取当前行的activities字段值 activities_str = self.get_field_value(instance, 'activities') if not activities_str: return # 分割成单个Activity条目 activity_entries = activities_str.split('/') for entry in activity_entries: entry = entry.strip() if not entry: continue # 解析日期和描述:按第一个'-'分割后,前3部分拼接为日期,剩余部分为描述 parts = entry.split('-', 3) if len(parts) < 3: continue # 格式错误,跳过该条目 date_str = f"{parts[0]}-{parts[1]}-{parts[2]}" description = parts[3].strip() if len(parts) >=4 else "" try: activity_date = datetime.strptime(date_str, '%Y-%m-%d').date() except ValueError: continue # 日期格式错误,跳过 # 创建或获取Activity(避免unique_together约束报错) Activity.objects.get_or_create( schedule=instance, date=activity_date, description=description )
关键说明
- 修正导出逻辑:原代码中
obj.projectactivity_set.all()是错误的,因为Activity的外键字段是schedule,反向关联默认是activity_set,需改为obj.activity_set.all()才能正确获取关联的Activity。 - after_save_instance方法:该方法在Project实例成功保存后触发,此时已经能获取到Project的ID,适合处理关联对象的创建。
- 解析Activity条目:根据导出的格式(
日期-描述用/分隔),通过分割字符串提取日期和描述,同时处理格式错误的情况避免导入失败。 - 避免重复创建:使用
get_or_create方法,结合Activity的unique_together约束,防止重复创建相同的Activity记录。 - dry_run判断:当进行导入预览(dry_run=True)时,不实际创建Activity,仅验证数据。
内容的提问来源于stack exchange,提问作者Willuq
相关产品推荐
相关产品推荐

