python-3.xdjangom2m

django remove m2m instance when there are no more relations


In case we had the model:

class Publication(models.Model):
    title = models.CharField(max_length=30)

class Article(models.Model):
    publications = models.ManyToManyField(Publication)

According to: https://docs.djangoproject.com/en/4.0/topics/db/examples/many_to_many/, to create an object we must have both objects saved before we can create the relation:

p1 = Publication(title='The Python Journal')
p1.save()
a1 = Article(headline='Django lets you build web apps easily')
a1.save()
a1.publications.add(p1)

Now, if we called delete in either of those objects the object would be removed from the DB along with the relation between both objects. Up until this point I understand.

But is there any way of doing that, if an Article is removed, then, all the Publications that are not related to any Article will be deleted from the DB too? Or the only way to achieve that is to query first all the Articles and then iterate through them like:

to_delete = []
qset = a1.publications.all()
for publication in qset:
    if publication.article_set.count() == 1:
        to_delete(publication.id)
a1.delete()
Publications.filter(id__in=to_delete).delete()

But this has lots of problems, specially a concurrency one, since it might be that a publication gets used by another article between the call to .count() and publication.delete().

Is there any way of doing this automatically, like doing a "conditional" on_delete=models.CASCADE when creating the model or something?

Thanks!


Solution

  • I tried with @Ersain answer:

    a1.publications.annotate(article_count=Count('article_set')).filter(article_count=1).delete()
    

    Couldn't make it work. First of all, I couldn't find the article_set variable in the relationship.

    django.core.exceptions.FieldError: Cannot resolve keyword 'article_set' into field. Choices are: article, id, title

    And then, running the count filter on the QuerySet after filtering by article returned ALL the tags from the article, instead of just the ones with article_count=1. So finally this is the code that I managed to make it work with:

    Publication.objects.annotate(article_count=Count('article')).filter(article_count=1).filter(article=a1).delete()
    

    Definetly I'm not an expert, not sure if this is the best approach nor if it is really time expensive, so I'm open to suggestions. But as of now it's the only solution I found to perform this operation atomically.