【问题标题】:How to delete many to many when unassociated in Django?在 Django 中未关联时如何删除多对多?
【发布时间】:2014-05-07 04:34:29
【问题描述】:

我们有一个标签系统供用户通过定义的标签过滤他们的文件。

以下是模型的设置方式:

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)

现在,因为不需要标签,所以当我们从文件中删除标签时,它们不会被删除。这会留下一堆标签保存在我们的数据库中,我们要清理它们。

我尝试在 Files 模型上重新定义 save 方法和 clean 方法。

我已尝试在 Files 模型上连接 m2m_changed 信号:https://docs.djangoproject.com/en/dev/ref/signals/#m2m-changed

我最后尝试的是 pre_save 信号:https://docs.djangoproject.com/en/dev/ref/signals/#pre-save

我计划遍历标签并删除带有空 files_set 的标签,但使用这些方法我无法可靠地解决这个问题(即我最终删除了未关联但即将关联的标签关联(因为 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)

但是,正如我所说,它会在添加标签之前删除它(如果添加了它,我们显然不想删除它)。

【问题讨论】:

  • 你确定this 不能解决你的问题吗?
  • 您认为将标签制作成ForeignKeys会更好吗? @yorodm 未标记的文件呢?一个标准的untagged 标签?此外,这将限制我们每个文件只有一个标签。
  • 如果您愿意更改代码,也许您可​​以使用django-taggit,它是 django-apps 的现成标记解决方案。
  • 检查this。我认为它会解决你的问题。 @bozdoz

标签: python django many-to-many


【解决方案1】:

总结一下,希望得到一个更清晰的答案:

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 确保在标签与文件解除关联时触发回调

【讨论】:

    【解决方案2】:

    您在使用 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 参数来获取将被删除的实际标签。

    更新

    当然,前一个监听器在删除文件时不会删除未引用的标签,因为它只处理来自标签模型的pre_clearpre_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)
    

    希望这能解决问题。

    【讨论】:

    • 我想我已经尝试过这样的事情了。我在添加和删除时遇到了问题。有时它会在添加标签之前删除它们,有时它会在未引用之前检查未引用的标签。无论如何,我会试一试,让你知道!
    • 很好奇,你为什么过滤 n_files=1 而不是 n_files=0?
    • 无论如何,它似乎适用于大多数场景,这很棒。但是,如果您删除该文件,它不会删除与该文件关联的标签。我想我们可以再做一次文件删除检查。
    • @bozdoz 我正在检查 n_files=1 因为我正在处理 pre_clear 和 pre_remove 事件,这意味着此时标签尚未与文件取消关联。另外,我更新了我的答案以处理删除文件对象的情况。
    • 很好的答案,很好的解决方案。不敢相信它没有内置到 django 中。
    【解决方案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)
    

    【讨论】:

    • 当你添加一个标签时,它最初是未引用的,所以这会在添加标签之前删除标签;因此,它会阻止添加标签。
    • 嗯,如果标签绑定到用户,如何删除?如您所见,我建议仅删除添加/编辑文件的特定用户的标签。用户创建文件并添加标签。标签将在文件之前保存。之后,您将保存文件。新标签不会被删除。或者你想先添加很多标签?并且不立即将它们绑定到文件?如果您不希望数据库中存在此类标签,也许您不应该允许用户创建与文件标签无关的标签?​​
    • 该方法在不需要时会被频繁触发,从而导致对数据库的不必要调用
    猜你喜欢
    • 2016-06-18
    • 2017-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多