6

Let's say I have following ORM classes (fields removed to simplify):

class Animal(models.Model):
    say = "?"

    def say_something(self):
        return self.say

class Cat(Animal):
    self.say = "I'm a cat: miaow"

class Dog(Animal):
    self.say = "I'm a dog: wuff"

class Animals(models.Model):
    my_zoo = models.ManyToManyField("Animal")

When I add some animals to my zoo:

cat = Cat()
cat.save()
dog = Dog()
dog.save()

animals.my_zoo.add(cat)
animals.my_zoo.add(dog)

for animal in animals.my_zoo.all():
    print animal.say_something()

... I would expect following result:

I'm a cat: miaow, I'm a dog: wuff

but instead, all I've got is the instances of general Animal object, unable to say anything but "?".

How to achieve the true object inheritance and later polymorphism when the object is retreived from db?

edkirin
  • 507
  • 1
  • 4
  • 11

2 Answers2

6

Model inheritance in django does not add any type information to the base class. So it is not really possible to down-cast objects down from Animal() to their appropriate forms.

Inheritance is used only to map fields on inherited model back to parent models. So if Animal has field name, the same field will exist on Cat and when you save Cat, the animal will be updated.

Inheritance works by adding a OneToOne relation:

class Animal(Model):
    name = CharField()

class Cat(Model):
    animal_ptr = OneToOneField(Animal)

Cat(name='Murky').save()

print repr(list(Animals.objects.all()))

: [Animal(name='Murky')]

Technically in your situation it is even possible for Animal() to be both Dog() and Cat() at the same time:

animal = Animal()
animal.save()

Cat(animal_ptr=animal).save()
Dog(animal_ptr=animal).save()

The way to solve your problem would be to add a field subtype or similar to your Animal() object and implement downcasting function:

class Animal(Model):
    subtype = CharField()

    def downcast(self):
        if self.subtype == 'cat':
            return self.cat
            # The self.cat is a automatic reverse reference created
            # from OneToOne field with the name of the model

for animal in Animal.objects.all().select_related('dog', 'cat', ...)]:
    animal.downcast().say_something()

A few useful reads on stack overflow with similar topics: Generic many-to-many relationships he hood. How to do Inheritance Modeling in Relational Databases?

Community
  • 1
  • 1
Ski
  • 14,197
  • 3
  • 54
  • 64
  • 1
    +1 for a good explanation; Even though I find that adding a `species_name` is a hack, there is no better way (in fact, I did use this myself). Also, if you ask me, django should do something like that internally, and not make us hit our heads against the keyboard. – Gabi Purcaru Nov 10 '10 at 20:01
  • Thanks for the answer. I understand how it works, but I was hoping that there is some magic which will do dirty work for me. – edkirin Nov 11 '10 at 07:12
0

You can access the descendentas via an attribute on the parent class. The attribute's name is the lowercase version of the model's name:

class Animal(models.Model):
    say = "?"

    def say_something(self):
        for animal in ('cat', 'dog'):
            try:
                return getattr(self, animal).say
            except:
                pass
            return self.say
Bernhard Vallant
  • 49,468
  • 20
  • 120
  • 148