lpares12
lpares12

Reputation: 4013

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!

Upvotes: 4

Views: 568

Answers (2)

lpares12
lpares12

Reputation: 4013

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.

Upvotes: 2

Ersain
Ersain

Reputation: 1520

You can remove the related objects using this query:

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

annotate creates a temporary field for the queryset (alias field) which aggregates a number of related Article objects for each instance in the queryset of Publication objects, using Count function. Count is a built-in aggregation function in any SQL, which returns the number of rows from a query (a number of related instances in this case). Then, we filter out those results where article_count equals 1 and remove them.

Upvotes: 0

Related Questions