如何在Django中删除未关联的多对多关系?
我们有一个标签系统,用户可以通过定义的标签来过滤他们的文件。
下面是模型的设置:
class Tags(models.Model):
name = models.CharField(max_length=100)
user = models.ForeignKey(User)
class Files(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=100)
tags = models.ManyToManyField(Tags, null=True, blank=True)
现在,因为标签不是必需的,当我们从文件中移除标签时,这些标签不会被删除。这就导致我们的数据库里保存了一堆标签,而我们想要清理这些标签。
我尝试过重新定义文件模型的保存方法,以及清理方法。
我还尝试在文件模型上连接一个 m2m_changed 信号:https://docs.djangoproject.com/en/dev/ref/signals/#m2m-changed
我最后尝试的是一个 pre_save 信号:https://docs.djangoproject.com/en/dev/ref/signals/#pre-save
我原本打算遍历这些标签,删除那些没有关联文件的标签,但使用这些方法我无法可靠地判断(也就是说,我最终会删除一些标签,这些标签虽然现在没有关联,但马上就要被关联了,因为 m2m_changed 会因为不同的操作多次触发)。
这是我认为可行的方案:
def handle_tags (sender, instance, *args, **kwargs) :
action = kwargs.get('action')
if action == 'post_clear':
# search through users tags... I guess?
tags = Tags.objects.filter(user=instance.user)
for tag in tags:
if not tag.files_set.exists():
tag.delete()
return
m2m_changed.connect(handle_tags, sender=Files.tags.through)
但是,正如我所说的,这样会在标签被添加之前就删除它(如果它被添加了,我们显然不想删除它)。
3 个回答
我觉得你想得有点复杂了。只需要为Tag定义一个related_name,然后处理File的post_save信号就可以了。
class Files(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=100)
tags = models.ManyToManyField(Tags, null=True, blank=True, related_name='files')
def clean_empty_tags(sender, instance, *args, **kwargs):
Tags.objects.filter(user=instance.user, files=None).delete()
post_save.connect(clean_empty_tags, sender=Files)
简单总结一下,希望能更清楚一些:
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
class Tags(models.Model):
name = models.CharField(max_length=100)
user = models.ForeignKey(User)
class Files(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=100)
tags = models.ManyToManyField(Tags, null=True, blank=True)
@receiver(m2m_changed, sender=Files.tags.through)
def delete_orphean_dateranges(sender, **kwargs):
if kwargs['action'] == 'post_remove':
Tags.objects.filter(pk__in=kwargs['pk_set'], files_set=None).delete()
post_remove
确保在一个标签和文件断开关联时,会触发相应的回调函数。
你在使用 m2m_changed 信号时走在正确的方向上。
你的问题是,当你响应 post_clear
信号时,标签已经被删除了,所以你无法像那样访问它们。
其实你需要在标签被删除之前就执行你的方法,这就意味着要处理 pre_clear
信号。
可以这样做:
@receiver(m2m_changed, sender=Files.tags.through)
def handle_tags(sender, **kwargs):
action = kwargs['action']
if action == "pre_clear":
tags_pk_set = kwargs['instance'].tags.values_list('pk')
elif action == "pre_remove":
tags_pk_set = kwargs.get('pk_set')
else:
return
# I'm using Count() just so I don't have to iterate over the tag objects
annotated_tags = Tags.objects.annotate(n_files=Count('files'))
unreferenced = annotated_tags.filter(pk__in=tags_pk_set).filter(n_files=1)
unreferenced.delete()
我还添加了对 pre_remove
信号的处理,你可以用 pk_set
参数来获取将要被删除的实际标签。
更新
当然,之前的监听器在删除文件时不会删除那些没有被引用的标签,因为它只处理 Tags 模型的 pre_clear
和 pre_remove
信号。为了实现你想要的效果,你还应该处理 Files 模型的 pre_delete
信号。
在下面的代码中,我添加了一个工具函数 remove_tags_if_orphan
,这是 handle_tags
的稍微修改版,还有一个新的处理器 handle_file_deletion
,用于删除那些在文件被删除后会变得没有引用的标签。
def remove_tags_if_orphan(tags_pk_set):
"""Removes tags in tags_pk_set if they're associated with only 1 File."""
annotated_tags = Tags.objects.annotate(n_files=Count('files'))
unreferenced = annotated_tags.filter(pk__in=tags_pk_set).filter(n_files=1)
unreferenced.delete()
# This will clean unassociated Tags when clearing or removing Tags from a File
@receiver(m2m_changed, sender=Files.tags.through)
def handle_tags(sender, **kwargs):
action = kwargs['action']
if action == "pre_clear":
tags_pk_set = kwargs['instance'].tags.values_list('pk')
elif action == "pre_remove":
tags_pk_set = kwargs.get('pk_set')
else:
return
remove_tags_if_orphan(tags_pk_set)
# This will clean unassociated Tags when deleting/bulk-deleting File objects
@receiver(pre_delete, sender=Files)
def handle_file_deletion(sender, **kwargs):
associated_tags = kwargs['instance'].tags.values_list('pk')
remove_tags_if_orphan(associated_tags)
希望这样能让事情更清楚。