django-elasticsearch-dsl icon indicating copy to clipboard operation
django-elasticsearch-dsl copied to clipboard

Deleting many to many with through table does not work

Open piedrahitapablo opened this issue 5 years ago • 2 comments

Hi, I think I found a bug when working with many to many fields with a through table.

Here is the thing, I'm currently working on a project that has an Organization model, this model has a many to many field related to an Agent model through a Membership model.

Membership is listed as a related model on the corresponding Organization document and in the get_instances_from_related is checked if the related instance is a Membership object.

To add a new member to the organization a call to Membership.objects.create(...) is made and in this case the indexing process work fine, the new agent is added as a member on the corresponding document. The problem arises when I try to remove a member from the organization using the .delete() method of the Membership instance, the agent is not removed from elasticsearch.

I checked and when I remove an agent from the organization, the Organization object that I get in the get_instances_from_related method still lists the removed agent as a member of the organization. My guess is that this problem is caused by using a pre_delete signal for related instances.

Membership.objects.create(...) and .delete() are used since django disables the .add() and .remove() methods on many to many managers for fields with a through table, also the m2m_changed signal is not dispatched for this fields.

Thanks in advance for your help and for developing this library, it has been very useful for us.

piedrahitapablo avatar Mar 12 '19 23:03 piedrahitapablo

If someone else is having this problem, it can be solved defining a simple post_delete signal, this can serve as an example:

from django.db.models.signals import post_delete
from .documents import OrganizationDocument
from .models import Membership, Organization

def update_elasticsearch_after_removing_member(sender, instance, **kwargs):
    organization = instance.organization
    doc = OrganizationDocument()
    doc.update(organization)


post_delete.connect(
    update_elasticsearch_after_removing_member, sender=Membership
)

piedrahitapablo avatar Mar 13 '19 14:03 piedrahitapablo

I've also encountered this issue. As a foolproof strategy, while less efficient, I find that inheriting a signal processor from RealTimeSignalProcessor and defining handle_delete as such, addresses the above issue -- note that it is less efficient as delete_related() is called twice (once in handle_pre_delete, and once below), this ensures that any get_instances_from_related calls that query the DB continue to work properly pre-delete AND all objects that need to remove the relevant "through" objects post-delete do so properly:

    def handle_delete(self, sender, instance, **kwargs):
        """Handle delete.
        Given an individual model instance, delete the object from index.
        """
        registry.delete(instance, raise_on_error=False)

        # we must call delete related again here (after the delete) to ensure that related instances don't persist the deleted 
        # object, e.g. when an m2m object is deleted
        registry.delete_related(instance)

If someone else has a more modular/efficient solution here, feel free to share! :)

Taskle avatar Aug 20 '20 21:08 Taskle