Доброго времени! По наследованию моделей в документации есть очень избыточная информация, и в этой статье не будет копипаста. Но, как и любая документация, документация джанго дает довольно дозированно паттерны ее применения. О них и пойдет речь в данной статье: Итак, небольшой ликбез: вкратце пройдемся по всем способам наследования моделей, которое нам предоставляет фреймворк:
- Абстрактное наследование — абстрактная модель не создает таблицы. Используется для сокращения объема кода и лаконичного стиля его написания
- Multitable наследование — довольно интересный вид наследования. Это наследование требует особого внимания: родительская модель создает таблицу со всеми полями, как обычная модель. Но! Она будет хранить в себе кроме своих значений так же значения полей всех моделей, наследованных от нее. Что касается дочерних моделей, то они создают таблицы с полями, определенными непосредственно в них — без унаследованных полей — + поле с именем вида имяМодели_ptr_id. Это поле является первичным ключом к родительской модели и является уникальным.
- Proxy-наследование — позволяет только переопределение поведения в дочерней модели
- Так же считаю, стоит отметить работу с GenericForeignKey, хотя этот паттерн и не относится к наследованию моделей, но очень тесно с ним связан
Абстрактное наследование:
Абстрактное наследование реализуется через атрибут abstract подкласса Meta нашей модели, например:
class Note(models.Model): class Meta: abstract=True ordering = ['Time'] verbose_name = u'Запись' verbose_name_plural = u'записи' Content = models.TextField(verbose_name=u'содержание') Time = models.DateTimeField(auto_now=True, verbose_name=u'Время') From = models.ForeignKey(Profile, related_name = '%(class)s')
В дальнейшем это может сократить нам количество кода для следующих моделей:
class Comment(Note): class Meta: verbose_name = u'Комментарий' verbose_name_plural = u'комментарий' Target = models.ForeignKey('Article') class Article(Note): class Meta: verbose_name = u'Заметка' verbose_name_plural = u'заметка' Title = models.CharField(max_length=100, verbose_name=u'Заголовок') Category = models.ManyToManyField('Mark', verbose_name=u'Категория', related_name='Articles')
Хорошо. Не надо для каждой модели указывать поля Content, Title и From. На этом плюсы заканчиваются и начинаются ограничения:
- Абстрактную модель нельзя добавить в ModelAdmin
- К абстрактной модели нельзя создавать отношения ForeignKey (ну и ManyToMany соответственно)
- Нельзя использовать константный related_name (для этого есть решение в виде
'%(class)s'
) - Она не имеет менеджера и так далее
Она позволяет на уровне Python разделить общие данные, используя в то же время одну таблицу в базе данных
MultiTable наследование:
Допустим, у вас есть модель «Запись»:
class Note(models.Model): class Meta: ordering = ['Time'] verbose_name = u'Запись' verbose_name_plural = u'записи' Content = models.TextField(verbose_name=u'содержание') Time = models.DateTimeField(auto_now=True, verbose_name=u'Время') From = models.ForeignKey(Profile, related_name='Notes') def __unicode__(self): when = "%H:%M" if self.Time.day == datetime.date.today().day else "%d.%m.%Y" return u'%s от %s' % (u'Запись ', self.Time.strftime(when))
И вы хотите расширить ее, добавив к ней поле Target:
class Comment(Note): class Meta: verbose_name = u'Комментарий' verbose_name_plural = u'комментарий' Target = models.ForeignKey('Comment')
Теперь в таблице будет создано две таблицы main_comment и main_note. Структуру таблиц я попробовал проиллюстрировать на скриншоте:
Такой подход к наследованию кардинально меняет дело, поскольку позволяет применять такой важный параметр ооп, как полиморфизм: снимаются почти все ограничения абстрактного наследования. Вы можете использовать родителей в ForeignKey, ModelAdmin и т д. При чем получая query_set базового класса — вы получаете и все объекты, которые от него унаследованы! То есть Note.objects.all()
вернет и Comment-ы так же. Это похоже на полиморфизм в C# и C++. Единственное, чего не хватало мне, так это опции «абстрактный класс». Питон ввел это понятие абстрактных классов (ABC) довольно поздно (2.6), намного позже других строго типизированных языков. Но тем не менее, это стало для него хорошим паттерном.
Что дает понятие «абстрактного класса»? В C++ и C# абстрактный класс не может быть инициализирован, подобно абстрактной модели Джанго. Зачем это нужно? Вернемся к моделям Note, Comment и Article. И введем еще одну модель — Raiting:
class Raiting(models.Model): From = models.ForeignKey(Profile, related_name='passed_Marks') Target = models.ForeignKey(Note, related_name='gained_Marks', verbose_name=u'Запись')
Здесь мы хотим определить, что статьи и комментарии можно оценивать. Вто же время Note — это абстрактное понятие, которого не должно существовать в реальном мире. Если в мета-классе прописать abstract=True
, то мы получим ошибку поля Target модели Raiting.
Поразмыслив, я пришел к следующему решению, которое решил использовать в своих проектах. Это решение нельзя назвать наиболее оптимальным, но оно работает. Сперва я просто экспериментировал: определил класс, наследованный от Model и переопределим ему save:
class DjangoInterface(models.Model): def save(self, *args, **kwargs): if type(self) is Note: raise Exception(u'INote является абстрактным объектом') else: super(Note, self).save(args, kwargs)
В целом он вполне себе отсеил Note. Но не буду же я писать для каждой модели-интерфейса класс-родитель. Чтобы унифицировать этот паттерн, я решил декларировать для проекта следующий протокол: если название модели начинается с заглавной I и вторая буква тоже заглавная (по рекомендациям написания кода в C#), то эта модель будет являться абстрактной. Просто — не правда ли? И мы можем это сделать так же через save:
class DjangoInterface(models.Model): def save(self, *args, **kwargs): name = type(self).__name__ if len(name )>1: if name[0] == 'I' and name[1].isupper(): raise Exception(u'INote является абстрактным объектом') super(Note, self).save(args, kwargs)
Чтобы это использовать, достаточно унаследовать свою модель от DjangoInterface, и что в итоге мы получим:
class Note(DjangoInterface): class Meta: ordering = ['Time'] verbose_name = u'Запись' verbose_name_plural = u'записи' Content = models.TextField(verbose_name=u'содержание') Time = models.DateTimeField(auto_now=True, verbose_name=u'Время') From = models.ForeignKey(Profile, related_name='Notes') def __unicode__(self): when = "%H:%M" if self.Time.day == datetime.date.today().day else "%d.%m.%Y" return u'%s от %s' % (u'Запись ', self.Time.strftime(when))
Теперь мы сможем заполнять таблицу Note смело моделями Comment и Article и не бояться, что там будут Note, которых нет.
В принципе, чуть позже были найдены и альтернативные, более эффективные варианты без использования ABC. Их преимущество в том, что они могли бы определить атрибут isinterface для каждой модели на начало запуска программы и тогда не пришлось бы делать проверку при каждой инициализации модели. Но это уже детали реализации…
И да, забыл упомянуть, что модели джанго не умеют работать с ABC. Впрочем, если объединить ABC и ModelBase в одном классе через множественное наследование, может быть, и заработали бы…
В какой-то степени может показаться, что мультитэйбл-наследование подобно отношению OneToOneField двух моделей. И в принципе, это так. По сути, они выполняют одну роль. Пример:
class Abstr(models.Model): Width = models.CharField(max_length=1) Height = models.CharField(max_length=1) class ANewAbstr(Abstr): Deep = models.CharField(max_length=1) class Abstr1(models.Model): Width = models.CharField(max_length=1) Height = models.CharField(max_length=1) Child = models.OneToOneField('ANewAbstr1') class ANewAbstr1(models.Model): Deep = models.CharField(max_length=1)
Но есть некоторые отличия:
- При мультитэйбл наследовании дочерняя модель (ANewAbstr в нашем случае) не будет иметь обычной id-колонки в качестве первичного ключа, вместо нее будет колонка
имяродительскоймодели_ptr_id
, в нашем случаеabstr_ptr_id.
И вы не сможете обращаться с этой таблицей как с отдельной моделью через джанго-ОРМ. - При OneToOne же — создается отдельная колонка для хранения id связанной модели — Child_id, а значит такая схема с OneToOne будет занимать больше места в БД.
Что еще может пригодиться? При работе с multitable-наследованием вы можете столкнуться с подобной ситуацией, когда надо преобразовать родительский класс в дочерний. Одно из решений выглядит так:
Parent = Abstr.objects.get(pk=1) Child = ANewAbstr(place_ptr=Parent , ...)
Как видно, это довольно лаконично и эффективно.
Proxy-наследование
Позволяет изменить поведение модели, но не более. Пример использования:
class MyNote(Note): class Meta: proxy = True
По сути, этот способ противоположен абстрактному наследованию: при абстрактном наследовании моделей джанго не создает таблицы для родителя. В случае с прокси-наследованием — напротив — не создается таблицы для модели-наследника.
Но при этом абстрактное наследование обладает гораздо большим могуществом, поскольку позволяет создавать и наследовать поля моделей. А proxy — может менять только поведение.
Множественное наследование моделей
Множественное наследование можно разделить на множественное наследование с Mixin-классами и полноценное множественное наследование. Это довольно ответственная тема. Поэтому я прошу относиться к ней серьезно. Тем более, что в сети о нет почти никакой информации:
Полноценное множественное наследование Django
В целом оф. документация django не предусматривает множественное наследование моделей. При попытке это сделать, вы получите TypeError: Cannot create a consistent method resolution order (MRO). Для того, чтобы решить эту проблему, необходимо переопределить models.Model и унаследоваться от новой Model. При чем такое переопределение заслуживает отдельной статьи и глубокого знания python, в частности работы Metaclass-а
Множественное наследование модели с Mixin
В сети есть несколько тем по этому вопросу. Mixin изначально подразумевает какой-то класс, расширяющий функционал — что-то наподобие интерфейса с дефолтной реализацией. Например такой:
class MixinModel: @classmethod def All(cls): return cls.objects.all() @classmethod def Find(cls, *args, **kwargs): return cls.objects.filter(*args, **kwargs) class Profiles(AbstractUser, MixinModel): City = models.CharField(max_length=90, verbose_name='Город')
И в принципе это работает. Но это недокументированный способ. Он у меня работал ровно до того момента, пока я не стал делать миграции: джанго пытался мигрировать MixinModel. Ну а так как MixinModel — это не модель, то… у него ничего не получилось. И даже не пытайтесь обойти это в таком духе:
class MixinModel(models.Model): class Meta: abstract = True
Получите TypeError: Cannot create a consistent method resolution order (MRO). А проблема решается довольно просто и встретить ее можно только на 2-м питоне. Для решения достаточно унаследовать MixinModel от object, и все проблемы решатся сами собой:
class MixinModel(object): ...
GenericForeignKey
GenericForeignKey — не является вариантом наследования моделей. Это скорее вариант полиморфизма.
Как мы знаем идея полиморфизма в статически типизированных языках тесно связана с наследованием, и без наследования не живет в принципе. В моделях джанго для реализации полиморфизма был придуман атрибут GenericForeignKey — он позволяет назначить полю ForeignKey любой существующей модели.
Как это работает?
Итак, GenericForeignKey — это универсальное поле джанго-orm, которое может указывать на любой объект. Для этого внутри модели создается три поля, которые условно назовем content_type
типа ForeignKey, object_id
типа PositiveIntegerField и content_object
типа GenericForeignKey . content_type
ссылается на модель ContentType- встроенную модель в джанго, которая содержит все модели (точнее классы моделей) приложения. object_id будет хранить id объекта в соответствующей таблице ContentType. Ну а GenericForeignKey объединяет их оба, пример:
class TaggedItem(models.Model): tag = models.SlugField() content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id')
Несколько громоздко, но работает. Один из минусов такого подхода в том, что нет ограничений на модели, как при классическом полиморфизме, и при недосмотре можно указать неверную модель для content_object.
В сети я нашел такое решение через limit_choices_to:
class TaggedItem(models.Model): tag = models.SlugField() object_id = models.PositiveIntegerField() limit = models.Q(app_label='app_a', model='modela') \ | models.Q(app_label='app_b', model='modelb') content_type = models.ForeignKey(ContentType, limit_choices_to=limit) content_object = GenericForeignKey('content_type', 'object_id')
Оригинально не правда ли? Такой способ может быть альтернативой грамотному мультитэйбл наследованию. Или не может? — на этом у меня все. Свое мнение о том, что вы думаете по этому поводу, можете оставлять в комментариях