Django使用bulk_create批量创建时如何处理结合get_or_create的m2m字段
核心运行逻辑问题
原代码存在两个根本性的偏差,导致功能无法正常跑通、性能达不到批量创建的要求:
- 构造
Product实例时传入my_m2m_field参数属于无效操作:Django的M2M字段是基于双向主键的关联描述符,本身不是模型上可直接赋值的数据库字段,必须在两端对象都已经持久化到数据库、拿到实际主键后才能建立关联。直接把M2M字段当构造参数传入,会直接触发无效关键字参数的报错,根本执行不到bulk_create步骤。 - 逐行循环调用
get_or_create的方式会产生大量重复数据库查询:哪怕同一个标签在多行数据里重复出现,每次拆分字符串后都会单独查库、尝试创建,完全没有批量处理的优势,万级数据量下仅关联对象的查询就会产生数百上千次SQL请求。
正确的实现不需要绕开get_or_create的能力,只需要把整个流程拆成独立的批量处理阶段,配合through中间表的批量写入即可,全程不需要单条循环做数据库操作。
分阶段实现方案
1. 提前批量预处理所有关联的MyM2mModel对象
不要逐行拆分字符串就查库,先全量遍历待导入的行,把所有M2M字段的标签文本收集起来去重,一次性完成“查已有对象、批量创建缺失对象”的逻辑,建立标签文本到关联对象的映射字典,后续直接从字典取值即可,不需要再反复查库。
批量替代单条
get_or_create的核心逻辑:
- 用一次
filter(title__in=全量标签集合)查询,拿到所有已存在的MyM2mModel对象- 对比已存在的标签和全量标签的差值,把缺失的标签收集起来,做一次
bulk_create写入数据库- 把两类来源的对象合并成
{标签文本: 关联对象}的映射,全程仅需要2次左右的数据库查询,就能覆盖所有需要的关联对象。
2. 无关联批量创建Product对象
构造Product实例时,只传入普通标量字段(name、type),不要传任何M2M相关参数,调用bulk_create把所有Product写入数据库,接口会自动给每个实例回填数据库生成的主键,不需要手动指定id。
3. 通过through中间表批量建立关联关系
不管是Django自动生成的默认中间表,还是自定义声明的through模型,都可以直接拿到中间表的类,遍历每一行数据匹配对应的Product和MyM2mModel对象,构造所有中间表的实例,最后对中间表做一次bulk_create,批量写入所有关联关系,完全不需要循环调用add()方法。
修正后的完整代码
def create_objs(row_iter): # 阶段1:全量收集所有M2M标签,批量预处理关联对象 all_m2m_titles = set() row_m2m_map = {} for idx, row in enumerate(row_iter): m2m_str = row.get("m2m", "") if m2m_str: titles = [t.strip() for t in m2m_str.split(",") if t.strip()] row_m2m_map[idx] = titles all_m2m_titles.update(titles) else: row_m2m_map[idx] = [] # 批量获取已存在的MyM2mModel existing_m2m_qs = MyM2mModel.objects.filter(title__in=all_m2m_titles) existing_m2m_map = {obj.title: obj for obj in existing_m2m_qs} # 批量创建缺失的MyM2mModel,ignore_conflicts避免并发下重复创建 missing_titles = all_m2m_titles - set(existing_m2m_map.keys()) if missing_titles: new_m2m_objs = [MyM2mModel(title=t) for t in missing_titles] MyM2mModel.objects.bulk_create(new_m2m_objs, ignore_conflicts=True) # 将新创建的对象合并到映射表 for obj in new_m2m_objs: existing_m2m_map[obj.title] = obj # 阶段2:批量创建Product对象 product_objs = [ Product( name=row["name"], type=row["type"] ) for row in row_iter ] # 按实际数据量调整batch_size,避免单条SQL过大 products = Product.objects.bulk_create(product_objs, batch_size=500) # 阶段3:通过through中间表批量写入关联 # 如果是自定义through模型,直接替换为自己的中间表类即可 ThroughModel = Product._meta.get_field("my_m2m_field").through through_objs = [] for idx, product in enumerate(products): titles = row_m2m_map.get(idx, []) for title in titles: related_obj = existing_m2m_map.get(title) if not related_obj: continue # 中间表的外键字段名按照实际模型调整,默认格式为<主模型小写>、<关联模型小写> through_objs.append(ThroughModel( product=product, mym2mmodel=related_obj )) # 批量写入关联,ignore_conflicts避免重复关联报错 if through_objs: ThroughModel.objects.bulk_create(through_objs, batch_size=500, ignore_conflicts=True) return products
落地注意事项
- 中间表的外键字段名需要和实际模型匹配:如果是Django自动生成的默认中间表,两个外键默认对应主模型、关联模型的小写类名;如果是自定义的through模型,直接按照自己定义的字段名传参即可。
- 根据数据量调整
batch_size参数,避免单条SQL的数据包大小超过数据库的配置上限。 - 开启
ignore_conflicts=True需要保证关联字段设置了对应的唯一约束,比如MyM2mModel的title字段要设置unique=True,中间表要联合(product, mym2mmodel)设置唯一约束,才能保证冲突忽略的逻辑生效。 - 这套实现的整体性能比循环单条调用
obj.my_m2m_field.add()高1~2个数量级,把原本需要N次单条INSERT的操作,压缩成了固定几次批量INSERT操作。
内容的提问来源于stack exchange,提问作者Dibidalidomba
相关产品推荐
相关产品推荐

