Django m2m关系.add()/.set()方法生产环境未按预期工作
Django多对多关系MySQL环境重复关联问题
问题现象
- 本地开发环境使用SQLite数据库时,多对多(m2m)关系的
add()、set()操作、Admin面板管理均符合预期,重复执行add()不会生成重复关联 - 生产环境使用MySQL数据库时出现异常:对已存在的关联重复执行
add(),关联计数每次+1,会持续生成重复关联记录;set()方法无法正常生效,仅clear()方法可以清空关联 - Admin面板配置
raw_id_fields后,删除关联ID的操作无效果;无论底层存在多少条重复关联记录,form.cleaned_data['art_collections']返回的QuerySet始终仅包含1个对应实例
涉及的模型定义:
class Artwork(models.Model): ... art_collections = models.ManyToManyField("ArtCollection", related_name="artworks", blank=True)
触发异常的复现代码:
artwork = Artwork.objects.get(pk=1) art_collection = ArtCollection.objects.get(pk=1) artwork.art_collections.add(art_collection)
相关Admin配置代码:
def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) def save_related(self, request, form, formsets, change): super(ArtworkAdmin, self).save_related(request, form, formsets, change)
使用的表单定义:
from dal import autocomplete class ArtWorkForm(autocomplete.FutureModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) class Meta: model = Artwork fields = ( ... "art_collections", ) widgets = { ... } help_texts = { ... }
根本原因
Django多对多关系的自动去重逻辑,完全依赖框架自动生成的中间表上两个关联外键的联合唯一约束:
正常状态下,add()方法底层通过get_or_create写入关联记录,遇到已存在的关联会直接跳过,不会插入新行;如果这个联合唯一约束缺失,get_or_create无法匹配到已存在的记录,每次调用都会直接插入新行,最终产生重复关联。
本地SQLite环境运行正常,是因为本地执行迁移时正确创建了该约束;生产MySQL环境异常,核心原因就是该m2m中间表缺失了联合唯一约束,通常由迁移执行异常、历史m2m字段变更未正确生成迁移、手动修改过中间表结构导致。
说明:form.cleaned_data始终只返回1个实例属于正常表现——Django查询m2m关联时默认会加DISTINCT去重,哪怕底层存在多条重复记录,ORM返回的结果集也只会保留1个不重复的关联对象,和表单、Admin重写的方法逻辑无关,现有save_model、save_related、表单代码均无问题。
修复步骤
- 登录MySQL数据库,检查Django自动生成的m2m中间表(默认表名格式为
应用名_artwork_art_collections)的索引结构,确认是否存在artwork_id和artcollection_id两个字段的联合唯一索引,无该索引即可确认问题。 - 先清理表中已存在的重复关联记录,执行以下SQL(替换为实际的表名),每组重复关联保留ID最小的1条记录,删除其余重复项:
DELETE t1 FROM 你的应用名_artwork_art_collections t1 INNER JOIN 你的应用名_artwork_art_collections t2 WHERE t1.id > t2.id AND t1.artwork_id = t2.artwork_id AND t1.artcollection_id = t2.artcollection_id; - 给中间表添加联合唯一约束:
如果需要保证所有环境迁移一致性,可以新建Django迁移文件,给m2m中间表(可通过ALTER TABLE 你的应用名_artwork_art_collections ADD UNIQUE KEY `unique_artwork_collection` (`artwork_id`, `artcollection_id`);Artwork.art_collections.through获取对应的中间表模型)添加unique_together约束,后续部署时执行迁移即可自动完成约束添加。 - 验证修复结果:重复执行
add()操作不会再生成重复记录,set()方法、Admin面板的关联编辑、删除操作均可恢复正常。
内容的提问来源于stack exchange,提问作者Dudas
相关产品推荐
相关产品推荐

