Django's post_save signal behaves weirdly with models using multi-table inheritance
Question: Django's post_save signal behaves weirdly with models using multi-table
inheritance

I am noticing an odd behavior in the way Django's post_save signal works when
using a model that has multi-table inheritance.

I have these two models:

    
    
    class Animal(models.Model):
        category = models.CharField(max_length=20)
    
    class Dog(Animal):
        color = models.CharField(max_length=10)
    

I have a post save callback called echo_category:

    
    
    def echo_category(sender, **kwargs):
        print "category: '%s'" % kwargs['instance'].category
    post_save.connect(echo_category, sender=Dog)
    

I have this fixture:

    
    
    [
        {
            "pk": 1,
            "model": "animal.animal",
            "fields": {
                "category": "omnivore"
            }
        },
    
        {
            "pk": 1,
            "model": "animal.dog",
            "fields": {
                "color": "brown"
            }
        }
    ]
    

In every part of the program except for in the post_save callback the
following is true:

    
    
    from animal.models import Dog
    Dog.objects.get(pk=1).category == u'omnivore' # True
    

When I run syncdb and the fixture is installed, the echo_category function is
run. The output from syncdb is:

    
    
    $ python manage.py syncdb --noinput
    Installing json fixture 'initial_data' from '~/my_proj/animal/fixtures'.
    category: ''
    Installed 2 object(s) from 1 fixture(s)
    

The weird thing here is that the dog object's category attribute is an empty
string. Why is it not 'omnivore' like it is everywhere else?

As a temporary (hopefully) workaround I reload the object from the database in
the post_save callback:

    
    
    def echo_category(sender, **kwargs):
        instance = kwargs['instance']
        instance = sender.objects.get(pk=instance.pk)
        print "category: '%s'" % instance.category
    post_save.connect(echo_category, sender=Dog)
    

This works but it is not something I like because I must remember to do it
when the model inherits from another model and it must hit the database again.
The other weird thing is that I must do instance.pk to get the primary key.
The normal 'id' attribute does not work (I cannot use instance.id). I do not
know why this is. Maybe this is related to the reason why the category
attribute is not doing the right thing?


Answer: This is because data loaded from fixture with `loaddata`/`syncdb` command is
saved as _raw_ in the database: only the fields of the resulting model table
get saved, to avoid hitting the database for all the models in the class
hierarchy.

But, when a model is saved _as raw_ , you get an extra `raw` keyword argument
in your signal, so you can handle the case properly. Your signal would end up
like that:

    
    
    def echo_category(sender, **kwargs):
        if kwargs.get('raw', False):
            instance = sender.objects.get(pk=kwargs['instance'].pk)
        else:
            instance = kwargs['instance']
        print "category: '%s'" % instance.category
    
    post_save.connect(echo_category, sender=Dog)
    

This way, you will only get the extra database query when dealing with
fixtures (and I guess that's acceptable in your case).

About your other question:

> The other weird thing is that I must do instance.pk to get the primary key.
> The normal 'id' attribute does not work (I cannot use instance.id). I do not
> know why this is.

`id` and
[`pk`](http://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.pk)
have a slightly different semantic. In your example, the `id` of a `Dog`
object is the `AutoField` defined
([automagically](http://docs.djangoproject.com/en/dev/topics/db/models/#id1))
in your `Animal` class. The `pk` however, is an `OneToOneField` (once again,
[automagically
defined](http://docs.djangoproject.com/en/dev/topics/db/models/#id7)) in the
`Dog` class.

In practice, both field have the **same** value all the time. However, as `id`
is a field coming from `Animal`, it won't exists for `Dog` object saved as
_raw_.

Hope that helps.

**EDIT** : this issue has already been reported on django's trac
[here](http://code.djangoproject.com/ticket/13299).

