Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: find all reverse references by foreign keys

Well, now I'm using Django 1.6+

And I have a model:

class FileReference(models.Model):
    # some data fields
    # ...
    pass

class Person(models.Model):
    avatar = models.ForeignKey(FileReference, related_name='people_with_avatar')

class House(models.Model):
    images = models.ManyToManyField(FileReference, related_name='houses_with_images')

class Document(model.Model):
    attachment = models.OneToOneField(FileReference, related_name='document_with_attachment')

So, many other model will have a foreign key referring to the FileReference model.

But sometimes, the referring models is deleted, with the FileReference object left.

I want to delete the FileReference objects with no foreign key referencing.

But so many other places will have foreign keys.

Is there any efficient way to find all the references? i.e. get the reference count of some model object?

like image 480
Alfred Huang Avatar asked Mar 20 '15 02:03

Alfred Huang


1 Answers

I stumbled upon this question and I got a solution for you. Note, that django==1.6 is not supported any more, so this solution will probably work on django>=1.9

Lets say we are talking about 2 of the objects for now:

class FileReference(models.Model):
    pass

class Person(models.Model):
    avatar = models.ForeignKey(FileReference, related_name='people_with_avatar', on_delete=models.CASCADE)

As you can see in ForeignKey.on_delete documentation, when you delete the related FileReference object, the referenced object Person is deleted as well.

Now for your question. How do we do the revered? We want upon Person deletion that FileReference object will be removed as well.

We will do that using post_delete signal:

def delete_reverse(sender, **kwargs):
    try:
        if kwargs['instance'].avatar:
            kwargs['instance'].avatar.delete()
    except:
        pass

post_delete.connect(delete_reverse, sender=Person)

What we did there was deleting the reference in avatar field on Person deletion. Notice that the try: except: block is to prevent looping exceptions.

Extra:

The above solution will work on all future objects. If you want to remove all of the past objects without a reference do the following:

In your package add the following file and directories: management/commands/remove_unused_file_reference.py

from django.core.management.base import BaseCommand, CommandError


class Command(BaseCommand):

    def handle(self, *args, **options):

        file_references = FileReference.objects.all()
        file_reference_mapping = {file_reference.id: file_reference for file_reference in file_references}

        persons = Person.objects.all()
        person_avatar_mapping = {person.avatar.id: person for person in persons}


        for file_reference_id, file_reference in file_reference_mapping.items():
            if file_reference_id not in person_avatar_mapping:
                file_reference.delete()

When you done, call: python manage.py remove_unused_file_reference This is the base idea, you can change it to bulk delete etc...

I hope this will help to someone out there. Good Luck!

like image 178
Gal Silberman Avatar answered Sep 29 '22 11:09

Gal Silberman