22

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.

bunnyhero
  • 757
  • 2
  • 10
  • 23

2 Answers2

39

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)
Daniel Holmes
  • 1,952
  • 2
  • 17
  • 28
Yuji 'Tomita' Tomita
  • 115,817
  • 29
  • 282
  • 245
  • 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). – bunnyhero Jan 19 '11 at 20:20
  • 1
    Yeah, most people prefer sticking to the ORM :) – Yuji 'Tomita' Tomita Feb 09 '11 at 18:12
  • 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? – sgauri Mar 13 '18 at 10:57
  • @sgauri 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. – Yuji 'Tomita' Tomita Mar 21 '18 at 16:05
  • @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. – sgauri Mar 21 '18 at 16:23
14

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()
vasi1y
  • 765
  • 5
  • 13
  • Especially for larger data sets this is the better answer. However, there is a difference: this solution doesn't trigger signal listeners. – Tim Jun 15 '21 at 13:02
  • 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. – Emilio Jan 04 '22 at 13:10