99

How can I get a list of all the model objects that have a ForeignKey pointing to an object? (Something like the delete confirmation page in the Django admin before DELETE CASCADE).

I'm trying to come up with a generic way of merging duplicate objects in the database. Basically I want all of the objects that have ForeignKeys points to object "B" to be updated to point to object "A" so I can then delete "B" without losing anything important.

Thanks for your help!

erikcw
  • 10,787
  • 15
  • 58
  • 75
  • 1
    This [Django snippet](http://djangosnippets.org/snippets/2283/) is definitely worth checking out! – Nick Merrill Dec 05 '14 at 00:04
  • I'm trying to implement the exact same thing myself. Would you be willing to share your solution? especailly how did `set` the related object to point to the A ? – eugene Dec 20 '15 at 10:54

9 Answers9

94

Django <= 1.7

This gives you the property names for all related objects:

links = [rel.get_accessor_name() for rel in a._meta.get_all_related_objects()]

You can then use something like this to get all related objects:

for link in links:
    objects = getattr(a, link).all()
    for object in objects:
        # do something with related object instance

I spent a while trying to figure this out so I could implement a kind of "Observer Pattern" on one of my models. Hope it's helpful.

Django 1.8+

Use _meta.get_fields(): https://docs.djangoproject.com/en/1.10/ref/models/meta/#django.db.models.options.Options.get_fields (see reverse in the _get_fields() source also)

int_ua
  • 1,646
  • 2
  • 18
  • 32
robbles
  • 2,729
  • 1
  • 23
  • 30
  • 7
    The `all()` part will fail on a `OneToOneField`. You have to detect it somehow. – augustomen Nov 29 '12 at 14:30
  • 3
    It doesn't show ManyToMany connections and was deprecated. In Django 1.8+ it's recommended to be replaced with `_meta.get_fields()`: https://docs.djangoproject.com/en/1.10/ref/models/meta/#django.db.models.options.Options.get_fields (see `reverse` in the `_get_fields()` source also) – int_ua Mar 14 '17 at 11:09
  • 1
    Thanks for the edit @int_ua! I'm surprised this workaround remained compatible with Django as long as it did. – robbles Mar 15 '17 at 17:53
  • 5
    Following on the `_meta.get_fields()` tip, this was part of the solution for me: `links = [field.get_accessor_name() for field in obj._meta.get_fields() if issubclass(type(field), ForeignObjectRel)]` (given `from django.db.models.fields.related import ForeignObjectRel`) – floer32 May 31 '18 at 20:41
26

@digitalPBK was close... here is probably what you are looking for using Django's built-in stuff that is used in Django admin for displaying related objects during deletion

from django.contrib.admin.utils import NestedObjects
collector = NestedObjects(using="default") #database name
collector.collect([objective]) #list of objects. single one won't do
print(collector.data)

this allows you to create what the Django admin displays - the related objects to be deleted.

Mehak
  • 961
  • 9
  • 20
IMFletcher
  • 656
  • 9
  • 19
  • FWICT this doesn't work quite correctly. It appears to follow relationships that do not imply the deletion criteria, though I'm not sure why. – Catskul Jun 10 '15 at 18:20
9

links = [rel.get_accessor_name() for rel in a._meta.get_all_related_objects()]

You can then use something like this to get all related objects:

for link in links:
    objects = getattr(a, link.name).all()
    for object in objects:
        # do something with related object instance

From Django 1.10 offical docs:

MyModel._meta.get_all_related_objects() becomes:

[
    f for f in MyModel._meta.get_fields()
    if (f.one_to_many or f.one_to_one)
    and f.auto_created and not f.concrete
]

So by taking the approved example we would use:

links = [
            f for f in MyModel._meta.get_fields()
            if (f.one_to_many or f.one_to_one)
            and f.auto_created and not f.concrete
        ]

for link in links:
    objects = getattr(a, link.name).all()
    for object in objects:
        # do something with related object instance
Daniel Holmes
  • 1,952
  • 2
  • 17
  • 28
Bojan Jovanovic
  • 1,439
  • 2
  • 19
  • 26
  • Just to correct your answer a little bit ```python for link in links: objects = getattr(a, link.name).all() for object in objects: ``` – Nam Ngo Feb 02 '17 at 06:02
7

The following is what django uses to get all related objects

from django.db.models.deletion import Collector
collector = Collector(using="default")
collector.collect([a])

print collector.data
digitalPBK
  • 2,859
  • 25
  • 26
  • 1
    doesn't seem to cascade. haven't done enough testing around this particluar one to know the full differences, but see my answer for the cascading one. – IMFletcher Oct 15 '13 at 19:52
7

Give this a try.

class A(models.Model):
    def get_foreign_fields(self):
      return [getattr(self, f.name) for f in self._meta.fields if type(f) == models.fields.related.ForeignKey]
buckley
  • 2,060
  • 1
  • 17
  • 12
6

Django 1.9
get_all_related_objects() has been deprecated

#Example: 
user = User.objects.get(id=1)
print(user._meta.get_fields())

Note: RemovedInDjango110Warning: 'get_all_related_objects is an unofficial API that has been deprecated. You may be able to replace it with 'get_fields()'

Slipstream
  • 13,455
  • 3
  • 59
  • 45
  • get_fields doesn't return related objects. – iankit Aug 17 '16 at 14:04
  • It does return reverse connections, even on Django 1.8, see https://docs.djangoproject.com/en/1.8/_modules/django/db/models/options/#Options.get_fields – int_ua Mar 14 '17 at 11:06
5
for link in links:
    objects = getattr(a, link).all()

Works for related sets, but not for ForeignKeys. Since RelatedManagers are created dynamically, looking at the class name is easier than doing an isinstance()

objOrMgr = getattr(a, link)
 if objOrMgr.__class__.__name__ ==  'RelatedManager':
      objects = objOrMgr.all()
 else:
      objects = [ objOrMgr ]
 for object in objects:
      # Do Stuff
Kai
  • 269
  • 2
  • 4
2

Unfortunately, user._meta.get_fields() returns only relations accessible from user, however, you may have some related object, which uses related_name='+'. In such case, the relation would not be returned by user._meta.get_fields(). Therefore, if You need generic and robust way to merge objects, I'd suggest to use the Collector mentioned above.

Jakub QB Dorňák
  • 1,231
  • 1
  • 9
  • 6
1

Here is another way to get a list of fields (names only) in related models.

def get_related_model_fields(model):
    fields=[]
    related_models = model._meta.get_all_related_objects()
    for r in related_models:
        fields.extend(r.opts.get_all_field_names())
    return fields
JessieinAg
  • 355
  • 3
  • 10