You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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
            )

关键说明

  1. 修正导出逻辑:原代码中obj.projectactivity_set.all()是错误的,因为Activity的外键字段是schedule,反向关联默认是activity_set,需改为obj.activity_set.all()才能正确获取关联的Activity。
  2. after_save_instance方法:该方法在Project实例成功保存后触发,此时已经能获取到Project的ID,适合处理关联对象的创建。
  3. 解析Activity条目:根据导出的格式(日期-描述用/分隔),通过分割字符串提取日期和描述,同时处理格式错误的情况避免导入失败。
  4. 避免重复创建:使用get_or_create方法,结合Activity的unique_together约束,防止重复创建相同的Activity记录。
  5. dry_run判断:当进行导入预览(dry_run=True)时,不实际创建Activity,仅验证数据。

内容的提问来源于stack exchange,提问作者Willuq

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 19:20:27