11

Is there a way to determine what the 'real' class of a Django database object is, after it has been returned from a query for on a base class?

For instance, if I have these models...

class Animal(models.Model):
    name= models.CharField(max_length=128)

class Person(Animal):
    pants_size = models.IntegerField(null=True)

class Dog(Animal):
    panting_rate = models.IntegerField(null=True)

And create these instances...

Person(name='Dave').save()
Dog(name='Mr. Rufflesworth').save()

If I do a query like Animal.objects.all(), I end up with two Animal instances, not an instance of Person and an instance of Dog. Is there any way to determine which instance is of which type?


FYI: I already tried doing this...

isinstance(Animal.objects.get(name='Dave'),Person) # <-- Returns false!

But that doesn't seem to work.

Shawn Chin
  • 84,080
  • 19
  • 162
  • 191
Chris W.
  • 37,583
  • 36
  • 99
  • 136
  • 2
    Welcome to Dja... oh screw it. Here it is, plain and simple. Django model inheritance is broken, and can't be fixed without a lot of work. – Ignacio Vazquez-Abrams Mar 07 '11 at 22:03
  • Errr, oh dear, that's not exactly the answer I was hoping for. Does that mean a lot of work from the guys at the Django Project, or does that mean a lot of work by me to hack around the issue? – Chris W. Mar 07 '11 at 22:46

4 Answers4

18

We implemented our own cast() function that works quite well (Without ContentType's):

class Base(models.Model):
    """
    If your class needs the basics, like created date, modified date etc, then
    inherit from this Base class.
    """
    created = models.DateTimeField(_('Created'), auto_now_add=True)
    modified = models.DateTimeField(_('Modified'), auto_now=True)

    class Meta:
        abstract = True

    def __str__(self):
        return '%s [%s]' % (self.__class__.__name__, self.id)

    def get_class_name(self):
        return str(self.__class__.__name__).lower()

    def to_json(self, include_related=True):
        return {
            'id': self.id,
            'created': self.created.isoformat(),
            'modified': self.modified.isoformat(),
            'class_name': self.__class__.__name__
        }

    def cast(self):
        """
        This method is quite handy, it converts "self" into its correct child class. For example:

        .. code-block:: python

           class Fruit(models.Model):
               name = models.CharField()

           class Apple(Fruit):
               pass

           fruit = Fruit.objects.get(name='Granny Smith')
           apple = fruit.cast()

        :return self: A casted child class of self
        """
        for name in dir(self):
            try:
                attr = getattr(self, name)
                if isinstance(attr, self.__class__):
                    return attr
            except:
                pass
        return self
Travis Pawley
  • 189
  • 1
  • 2
  • This will break when a model contains a foreign key to itself. To fix this, use `if isinstance(attr, item.__class__) and type(attr) != type(item):` – hughes Nov 14 '12 at 21:33
  • _cast_ method won't work for longer chain of inheritance. For example, if we have `class GreenApple(Apple): ...`, _GreenApple_ won't be detected. The solution is to have a variable `finalattr = attr` in _if_ statement, and than return it afterwards. – Stevan Sep 27 '17 at 21:29
11

I had a similar problem in the past and eventually found a satisfactory solution thanks to this answer.

By implementing an abstract class that stores the real class and have it inherited by your parent class, once can cast each parent class instance to the actual type. (The abstract class used in that answer is now available in django-model-utils.)

For example, once you have the abstract class defined (or if you have django-model-utils), you can simply do:

class Animal(InheritanceCastModel):
    name= models.CharField(max_length=128)

class Person(Animal):
    pants_size = models.IntegerField(null=True)

class Dog(Animal):
    panting_rate = models.IntegerField(null=True)

Using it is trivial:

>>> from zoo.models import Animal, Person, Dog
>>> Animal(name='Malcolm').save()
>>> Person(name='Dave').save()
>>> Dog(name='Mr. Rufflesworth').save()
>>> for obj in Animal.objects.all():
...     print obj.name, type(obj.cast())
...
Malcolm <class 'zoo.models.Animal'>
Dave <class 'zoo.models.Person'>
Mr. Rufflesworth <class 'zoo.models.Dog'>
Community
  • 1
  • 1
Shawn Chin
  • 84,080
  • 19
  • 162
  • 191
  • 1
    Looks like I'm going to be going with django-model-utils' InheritanceCastModel. Thanks a lot! – Chris W. Mar 09 '11 at 18:38
  • Unless you're using Django < 1.2, you should not use `InheritanceCastModel` -- see comment on my answer. – DrMeers Mar 09 '11 at 20:04
10

Yes, this can be done -- you just need to query the automatic reverse OneToOneField relations. E.g.:

a = Animal.objects.select_related('person', 'dog')[0]
a = a.person or a.dog or a # whichever is not None
print a
print isinstance(a, Person)

The use of select_related here allows this to be done in a single query, rather than having to test for DoesNotExist exceptions when you access the subclass attributes/relations.

See also my answer here and the InheritanceManager in django-model-utils for a more elegant/long-term solution.

We're looking at ways of making this easier in Django's core.

Community
  • 1
  • 1
DrMeers
  • 4,117
  • 2
  • 36
  • 38
  • Thanks for your answer, for my purposes though I wont actually know ahead of time all the possible class names to try to reference, as such, I'll need to use django-model-utils. – Chris W. Mar 09 '11 at 18:37
  • 1
    That's why I recommended it for the long-term. Note however that you're better of the `InheritanceManager` I mentioned above than `InheritanceCastModel` as suggested in your accepted answer, unless you're using a version of Django older than 1.2 -- `InheritanceCastModel` is less convenient and less efficient. – DrMeers Mar 09 '11 at 20:03
2

To solve this, consider using django-polymorphic. It supports automatic downcasting of inherited models, works with ForeignKeys/ManyToMany fields and integrates in the admin too.

vdboor
  • 21,914
  • 12
  • 83
  • 96