Login

(Almost) Single table polymorphism in Django

Author:
julkiewicz
Posted:
April 10, 2011
Language:
Python
Version:
1.3
Score:
1 (after 3 ratings)

An emulation of "table per hierarchy" a.k.a. "single table inheritance" in Django. The base class must hold all the fields. It's subclasses are not allowed to contain any additional fields and optimally they should be proxies. They however may provide additional methods to operate on the declared field. The presented solution supports implicit inheritance, even across ForeignKeys, and ManyToManyFields. No additional database hits are imposed.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
from abc import abstractmethod
from django.db import models
from django.db.models import query


class PolymorphicQuerySet(query.QuerySet):
    
    def get(self, *args, **kwargs):
        if kwargs.has_key('model_class'):
            cls = kwargs.pop('model_class')
            kwargs['mod'] = cls.__module__
            kwargs['cls'] = cls.__name__
        return super(PolymorphicQuerySet, self).get(*args, **kwargs)
    
    def filter(self, *args, **kwargs):
        if kwargs.has_key('model_class'):
            cls = kwargs.pop('model_class')
            kwargs['mod'] = cls.__module__
            kwargs['cls'] = cls.__name__
        return super(PolymorphicQuerySet, self).filter(*args, **kwargs)


class PolymorphicManager(models.Manager):
    
    def get_query_set(self):
        return PolymorphicQuerySet(self.model, using=self._db)
    
    def get(self, *args, **kwargs):
        return self.get_query_set().get(*args, **kwargs)
    
    def filter(self, *args, **kwargs):
        return self.get_query_set().filter(*args, **kwargs)


class PolymorphicModel(models.Model):
    
    classes = dict()
    mod = models.CharField(max_length=50)
    cls = models.CharField(max_length=30)
    
    class Meta:
        abstract = True
    
    @staticmethod
    def __new__(cls, *args, **kwargs):
        if len(args) > len(cls._meta.fields):
            raise IndexError("Number of args exceeds number of fields")
        c = None
        m = None
        fields_iter = iter(cls._meta.fields)
        for val, field in itertools.izip(args, fields_iter):
            if field.name == 'cls':
                c = val
            elif field.name == 'mod':
                m = val
        if c is None:
            c = kwargs.get('cls', None)
        if m is None:
            m = kwargs.get('mod', None)
        if c is not None:
            assert m is not None
            m = str(m)
            c = str(c)
            if not Model.classes.has_key(c):
                cls = getattr(__import__(m, globals(), locals(), [c]), c)
                PolymorphicModel.classes[(m, c)] = cls
            else:
                cls = Model.classes[(m, c)]
        return super(PolymorphicModel, cls).__new__(cls, *args, **kwargs)
    
    def save(self, *args, **kwargs):
        if not self.cls:
            self.mod = self.__class__.__module__
            self.cls = self.__class__.__name__
            super(PolymorphicModel, self).save(*args, **kwargs)


class SomeBase(PolymorphicModel):

    a = IntegerField(default=0)
    b = IntegerField(default=0)
    
    @abstractmethod
    def something(self): pass


class SomeDerivedA(SomeBase):
    
    class Meta:
        proxy = True

    def something(self):
        self.a += 1


class SomeDerivedB(SomeBase):
    
    class Meta:
        proxy = True

    def something(self):
        self.b += 1


class SomeCollection(models.Model):
    
    values = models.ManyToManyField(SomeBase)


>>> a = SomeDerivedA()
>>> a.save()
>>> b = SomeDerivedB()
>>> b.save()
>>> x = SomeCollection()
>>> x.values = [a, b]
>>> x.save()

>>> y = SomeCollection.objects.get(pk=x.pk)
>>> y.values.objects[0].something()
>>> y.values.objects[0].a
1
>>> y.values.objects[0].b
0
>>> y.values.objects[1].something()
>>> y.values.objects[1].a
0
>>> y.values.objects[1].b
1

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

julkiewicz (on September 22, 2011):

Corrected a serious bug - using new instead of init to perform class changes.

#

facundo_olano (on October 25, 2011):

I think there are some imports missing, one is itertools, the other is Model; the second I don't know where to import from.

#

patseng (on July 10, 2013):

Any suggestions on how to resolve his:

  • NameError: global name 'Model' is not defined

@facundo_olano already mentioned it. Is there a solution?

#

Please login first before commenting.