dynamic upcast

392 views
Skip to first unread message

dadapapa

unread,
Sep 12, 2008, 10:33:26 AM9/12/08
to Django users
Dear all,


I am playing around with model inheritance introduced in django 1.0.
In particular, I am trying to achieve something that might be called
dynamic-casts (with reference to C++). Consider the following example
type hierarchy:

MediaObject
|
+-- ImageObject
| |
| +-- PngObject
| +-- GifObject
|
+-- AudioObject
|
+-- Mp3Object
+-- WavObject


I want to retreive a set of MediaObjetcs from the database and treat
them with respect to their actual type. E.g. I want to display them
using specialized templates, like

{% for object in objects %}
{% include object.template %}
{% endfor %}

(or using some more sophisticated template dispatcher).

The problem I am running into is the following: when retrieving
MediaObjects from the database, e.g. via

obj = get_objet_or_404(MediaObject,id=1)

they become upcasted to actual MediaObjects no matter what their
type has been on object creation, and I cannot pass the correct type
-- get_object_or_404(Mp3Object,id=1) -- nor access the child object as
the attribute obj.mp3object -- simply because the actual type is
unknown
on retrieval.

I tried to use the contenttype application and hold a reference to the
original objects type in MediaObject:

class MediaObject(models.Model) :
final_type = models.ForeignKey(ContentType)

def __init__(self,*args) :
super(MediaObject,self).__init__(*args)
self.final_type =
ContentType.objects.get_for_model(type(self))

class AudioObject(MediaObject) :
pass

so that I could dynamically upcast objects to their original type with

obj =
get_object_or_404(MediaObject,id=1).final_type.get_object_for_this_type(id=1)

It does not work at all. For some reason, final_type also gets
upcasted
to MediaObject?!

Is there any (possibly elegant) way to achieve this functionality?

Thanks!

- harold -

dadapapa

unread,
Sep 12, 2008, 2:57:14 PM9/12/08
to Django users
> It does not work at all. For some reason, final_type also gets
> upcasted to MediaObject?!

I found the solution, now. The problem was that the __init__ method of
a Model is also called when objects are restored from the database. So
when the base class gets initialized by the QueryManager, it
overwrites final_type with the wrong value. The solution is simple:
overwrite the save method instead of __init__. Here is a complete
recipe:

class BaseClass(models.Model) :
final_type = models.ForeignKey(ContentType)

def save(self,*args) :
self.final_type =
ContentType.objects.get_for_model(type(self))
super(BaseClass,self)save(*args)

def cast(self) :
return
self.final_type.get_object_for_this_type(id=self.id)

class DerivedClass(ParentClass) :
pass

Here is an example:

obj = DerivedClass()
obj.save()

obj = get_object_or_404(BaseClass, id=3).cast()
# obj is now of type DerivedClass

Best,

- harold -

Malcolm Tredinnick

unread,
Sep 16, 2008, 8:17:31 AM9/16/08
to django...@googlegroups.com

On Fri, 2008-09-12 at 11:57 -0700, dadapapa wrote:
> > It does not work at all. For some reason, final_type also gets
> > upcasted to MediaObject?!
>
> I found the solution, now. The problem was that the __init__ method of
> a Model is also called when objects are restored from the database. So
> when the base class gets initialized by the QueryManager, it
> overwrites final_type with the wrong value. The solution is simple:
> overwrite the save method instead of __init__. Here is a complete
> recipe:
>
> class BaseClass(models.Model) :
> final_type = models.ForeignKey(ContentType)
>
> def save(self,*args) :

For absolute robustness, you should also accept **kwargs here. There are
a couple of places in Django's code that will call save() and pass in
force_insert=True, for example, which won't be handled by *args. In
reality, all you need to be able to handle is force_insert and
force_update, but *args and **kwargs are also pretty useful,
particularly if you aren't doing anything with them except passing them
along.

> self.final_type =
> ContentType.objects.get_for_model(type(self))
> super(BaseClass,self)save(*args)
>
> def cast(self) :
> return
> self.final_type.get_object_for_this_type(id=self.id)
>
> class DerivedClass(ParentClass) :
> pass
>
> Here is an example:
>
> obj = DerivedClass()
> obj.save()
>
> obj = get_object_or_404(BaseClass, id=3).cast()
> # obj is now of type DerivedClass

Yes, well done. This looks like the right way to handle this. Glad
somebody's documented it here.

Regards,
Malcolm


dadapapa

unread,
Sep 16, 2008, 8:32:39 PM9/16/08
to Django users
Hi Malcolm,

Malcolm Tredinnick wrote:
> > class BaseClass(models.Model) :
> > final_type = models.ForeignKey(ContentType)
> >
> > def save(self,*args) :
>
> For absolute robustness, you should also accept **kwargs here. There are
> a couple of places in Django's code that will call save() and pass in
> force_insert=True, for example, which won't be handled by *args. In
> reality, all you need to be able to handle is force_insert and
> force_update, but *args and **kwargs are also pretty useful,
> particularly if you aren't doing anything with them except passing them
> along.

thanks for the suggestion. What you say is, of course, true.

> > self.final_type =
> > ContentType.objects.get_for_model(type(self))
> > super(BaseClass,self)save(*args)
> >
> > def cast(self) :
> > return
> > self.final_type.get_object_for_this_type(id=self.id)
> >
> > class DerivedClass(ParentClass) :
> > pass
> >
> > Here is an example:
> >
> > obj = DerivedClass()
> > obj.save()
> >
> > obj = get_object_or_404(BaseClass, id=3).cast()
> > # obj is now of type DerivedClass
>
> Yes, well done. This looks like the right way to handle this. Glad
> somebody's documented it here.

Thanks. In principle, I think there is still room for optimization:
the database gets hit twice, and it would be sufficient to return
only the 'final_type' field in the first call, especially if the base
class
is voluminous. Since this is possible to do it in SQL, it shouldn't
be hard to do it in django, too, but I didn't have time to implement
this yet.

Cheers,

- harold -

Carl Meyer

unread,
Oct 19, 2008, 4:10:39 PM10/19/08
to Django users


On Sep 12, 2:57 pm, dadapapa <dadap...@googlemail.com> wrote:
>     class BaseClass(models.Model) :
>         final_type = models.ForeignKey(ContentType)
>
>         def save(self,*args) :
>             self.final_type =
> ContentType.objects.get_for_model(type(self))
>             super(BaseClass,self)save(*args)
>
>         def cast(self) :
>             return
> self.final_type.get_object_for_this_type(id=self.id)
>
>     class DerivedClass(ParentClass) :
>         pass

Just ran into this issue myself (first time using non-abstract
inheritance). The catch with this recipe is that you can never save a
derived object from a parent instance, or you break the final_type
field (it will then contain the parent class content type).

Carl

dadapapa

unread,
Oct 23, 2008, 9:27:46 AM10/23/08
to Django users
On Oct 19, 10:10 pm, Carl Meyer <carl.j.me...@gmail.com> wrote:
> Just ran into this issue myself (first time using non-abstract
> inheritance).  The catch with this recipe is that you can never save a
> derived object from a parent instance, or you break the final_type
> field (it will then contain the parent class content type).

If by "save a derived object from a parent instance" you mean that
the method that saves the object is defined in the parent class,
than this should not cause a problem, since type(self) will
dynamically identify the object as being of the derived type,
so final_type gets initialized correctly. Then again, I might have
misunderstood your problem.

Cheers,

- harold -

Carl Meyer

unread,
Oct 23, 2008, 12:32:13 PM10/23/08
to Django users
Hi harold,

On Oct 23, 9:27 am, dadapapa <dadap...@googlemail.com> wrote:
> If by "save a derived object from a parent instance" you mean that
> the method that saves the object is defined in the parent class,
> than this should not cause a problem, since type(self) will
> dynamically identify the object as being of the derived type,
> so final_type gets initialized correctly. Then again, I might have
> misunderstood your problem.

Sorry, I wasn't clear. You may have a BaseClass instance that is
"really" a DerivedClass, and if you ever call save() from that
BaseClass instance (without casting it to a DerivedClass first), the
recipe will break. Code is clearer:

>>> d = DerivedClass.objects.create()
>>> d.final_type
<ContentType: derived class>
>>> d2 = BaseClass.objects.all()[0]
>>> d2.final_type
<ContentType: derived class>
>>> d2.save()
>>> d2.final_type
<ContentType: base class>

The fix is just to add an "if not self.id" in the save() method, so
the "final_type" is only set once, at creation time, not on every
save:

def save(self, *args, **kwargs) :
if not self.id:
self.final_type =
ContentType.objects.get_for_model(type(self))
super(BaseClass, self).save(*args)

Carl
Reply all
Reply to author
Forward
0 new messages