How do I remove multiple objects in a ManyToMany relationship based on a filter?
Asked Answered
C

2

25

Given these two Models:

class Item(models.Model):
    timestamp = models.DateTimeField()

class Source(models.Model):
    items = models.ManyToManyField(Item, related_name="sources")

I can find all of a Source's Items before a given time using this:

source.items.filter(timestamp__lte=some_datetime)

How do I efficiently remove all of the items that match that query? I suppose I could try something like this:

items_to_remove = list(source.items.filter(timestamp__lte=some_datetime))
source.items.remove(*items_to_remove)

but that seems bad.

Note that I do not want to delete these items, since they may also belong to other Sources. I just want to remove their relationship with the specific source.

Caldera answered 17/1, 2011 at 23:42 Comment(0)
B
41

I think you got it right on the money, except you don't need to convert to a list.

source.items.remove(*source.items.filter(*args))

The remove/add method looks like the following

remove(self, *objs)
add(self, *objs)

and the docs use add multiple examples in the form of [p1, p2, p3] so I'd wager the same goes for remove, seeing as the arguments are the same.

>>> a2.publications.add(p1, p2, p3)

Digging in a little more, the remove function iterates over *objs one by one, checking if it's of the valid model, otherwise using the values as PK's, then deletes the items with a pk__in, so I'm gonna say yes, the best way is to query your m2m table first for objects to delete then pass in those objects into the m2m manager.

    # django.db.models.related.py
    def _remove_items(self, source_field_name, target_field_name, *objs):
        # source_col_name: the PK colname in join_table for the source object
        # target_col_name: the PK colname in join_table for the target object
        # *objs - objects to remove

        # If there aren't any objects, there is nothing to do.
        if objs:
            # Check that all the objects are of the right type
            old_ids = set()
            for obj in objs:
                if isinstance(obj, self.model):
                    old_ids.add(obj.pk)
                else:
                    old_ids.add(obj)
            if self.reverse or source_field_name == self.source_field_name:
                # Don't send the signal when we are deleting the
                # duplicate data row for symmetrical reverse entries.
                signals.m2m_changed.send(sender=rel.through, action="pre_remove",
                    instance=self.instance, reverse=self.reverse,
                    model=self.model, pk_set=old_ids)
            # Remove the specified objects from the join table
            db = router.db_for_write(self.through.__class__, instance=self.instance)
            self.through._default_manager.using(db).filter(**{
                source_field_name: self._pk_val,
                '%s__in' % target_field_name: old_ids
            }).delete()
            if self.reverse or source_field_name == self.source_field_name:
                # Don't send the signal when we are deleting the
                # duplicate data row for symmetrical reverse entries.
                signals.m2m_changed.send(sender=rel.through, action="post_remove",
                    instance=self.instance, reverse=self.reverse,
                    model=self.model, pk_set=old_ids)
Bantam answered 18/1, 2011 at 1:43 Comment(5)
Thanks! I'll try that and see how well it works. Right now I'm using a single raw SQL statement (using "DELETE ... USING" in PostgreSQL, which I understand is nonstandard).Caldera
Yeah, most people prefer sticking to the ORM :)Gomuti
doesn't using remove on m2m fields delete them? OP says he does not want to delete but just want to remove the relationship. Could you provide clarification on this?Civility
@Civility this is an old post, but if you look at the django code there, it deletes on the through model, i.e m2m relationship. In the same way .add() doesn't add a related model but only the relationship, remove() should only delete the relationship, not the related model. I also think it's why django devs explicitly called this add/remove not create/delete.Gomuti
@Yuji'Tomita'Tomita yeah, you are right. I was having doubts between .clear()and .remove(), I understood it later that former is used to remove relationships of all related objects while later is for specific object. thanks for your response.Civility
W
16

According to current docs there is a through property that gives you an access to table that manages many-to-many relation, like so Model.m2mfield.through.objects.all()

So in terms of your example:

source.items.through.objects \
    .filter(item__timestamp__lte=some_datetime) \
    .delete()
Willing answered 21/10, 2020 at 20:32 Comment(2)
Especially for larger data sets this is the better answer. However, there is a difference: this solution doesn't trigger signal listeners.Beast
The listeners detail is important, but this solution should have more upvotes. I didn't even know this was available and saved me a lot of time/effort.Caw

© 2022 - 2024 — McMap. All rights reserved.