Должен ли я каким-либо образом избегать многотабличного (конкретного) наследования в Django?
Многие опытные разработчики рекомендуют не использовать наследование нескольких таблиц Django из-за его низкой производительности:
Джанго Гоча: конкретное наследство от Якоба Каплана-Мосса, основного участника Джанго.
Почти в каждом случае абстрактное наследование является лучшим подходом в долгосрочной перспективе. Я видел более чем несколько сайтов, разрушенных под нагрузкой, вызванной конкретным наследованием, поэтому я настоятельно рекомендую пользователям Django подходить к любому использованию конкретного наследования с большой долей скептицизма.
Два совка Джанго от Дэниела Гринфилда ( pydanny)
Многостоловое наследование, иногда называемое "конкретным наследованием", считается авторами и многими другими разработчиками плохой вещью. Мы настоятельно рекомендуем не использовать его.
Любой ценой каждый должен избегать наследования нескольких таблиц, так как это добавляет путаницу и существенные накладные расходы. Вместо многотабличного наследования используйте явные OneToOneFields и ForeignKeys между моделями, чтобы вы могли контролировать прохождение соединений.
Но без наследования нескольких таблиц я не могу легко
Ссылочная базовая модель в другой модели (необходимо использовать GenericForeignKey или обратную зависимость);
Получить все экземпляры базовой модели.
(не стесняйтесь добавлять больше)
Так что же не так с этим видом наследования в Джанго? Почему явные OneToOneFields лучше?
Насколько сильно страдает производительность от JOIN? Есть ли тесты, которые показывают разницу в производительности?
Не select_related()
позволяют нам контролировать, когда JOINs вызываются?
Я переместил конкретные примеры в отдельный вопрос, поскольку этот вопрос становится слишком широким, и добавил список причин для использования многотабличного наследования.
4 ответа
Во-первых, наследование не имеет естественного перевода в архитектуру реляционных баз данных (хорошо, я знаю, Oracle Type Objects и некоторые другие RDBMS поддерживают наследование, но django не использует эту функциональность)
На этом этапе обратите внимание, что django генерирует новые таблицы для подклассов и пишет многоleft joins
чтобы получить данные из этих "подстолов". И оставленные объединения не твои друзья. В высокопроизводительном сценарии, таком как игровой бэкенд или что-то еще, вы должны избегать этого и разрешать наследование "вручную" с помощью некоторых артефактов, таких как null, OneToOne или внешние ключи. В OneToOne
Сценарий, вы можете вызвать связанные таблицы напрямую или только если вам это нужно.
... НО...
"По моему мнению (TGW)", вы должны включить наследование моделей в свои корпоративные проекты, когда они попадают в ваш мир дискурса. Я делаю это и экономлю много времени на разработку для своих клиентов благодаря этой функции. Кроме того, код становится чистым и элегантным, что означает простоту обслуживания (обратите внимание, что в подобных проектах нет сотен или запросов в секунду)
Вопрос за вопросом
В: Что не так с этим видом наследования в Джанго?
A: Много таблиц, много левых соединений.
Q: Почему явные OneToOneFields лучше?
A: Вы можете получить прямой доступ к связанной модели без левых соединений.
В: Есть ли наглядные примеры (тесты)?
A: Нет сопоставимых.
Q: Разве select_related() не позволяет нам контролировать, когда вызывается JOIN?
A: Django объединяет необходимые таблицы.
Вопрос: Каковы альтернативы многостоловому наследованию, когда мне нужно сослаться на базовый класс в другой модели?
A: Аннулирование. Отношения OneToOne и множество строк кода. Это зависит от потребностей приложения.
Q: GenericForeignKeys лучше в этом случае?
A: Нет для меня.
В: Что если мне понадобится OneToOneField для базовой модели? A: Напишите это. С этим проблем нет. Например, вы можете расширить пользовательскую модель, а также иметь базовую модель OneToOne to User для некоторых пользователей.
Заключение
Вы должны знать стоимость написания и обслуживания кода без наследования модели, а также стоимость оборудования для поддержки приложений наследования модели и действовать соответственно.
Мир изменился.
Первое, что следует отметить, это то, что статья под названием Django gotcha: конкретному наследованию было почти четыре года, когда был задан этот вопрос; в 2014 году. Как системы Django, так и системы RDBM прошли большой путь с тех пор (например, MySQL 5.0 или 5.1 были широко используемыми версиями, а общая доступность 5.5 была еще через месяц).
Присоединяется слева от меня, присоединяется справа от меня
Это правда, что наследование нескольких таблиц в большинстве случаев приводит к дополнительным соединениям за кулисами. Но соединения не являются злом. Стоит отметить, что в правильно нормализованной базе данных вам почти всегда приходится присоединяться для получения всех необходимых данных. Когда используются правильные индексы, объединения не включают каких-либо существенных потерь производительности.
ВНУТРЕННЕЕ СОЕДИНЕНИЕ против ЛЕВОГО НАРУЖНОГО СОЕДИНЕНИЯ
Это действительно относится к наследованию нескольких таблиц, при других подходах можно избежать дорогостоящего соединения LEFT OUTER JOIN и выполнить вместо него INNER JOIN или, возможно, подзапрос. Но с наследованием нескольких таблиц вам отказано в выборе
Из того, что я понимаю, вы используете OneToOneField
на RelatedModel
к BaseModel
потому что, в конечном счете, вы хотите, чтобы связь между RelatedModel
и каждый Submodel1
в Submodel9
, Если это так, есть более эффективный способ сделать это без наследования нескольких таблиц или общих отношений.
Просто избавься от BaseModel
и в каждом SubmodelX
есть OneToOneField
в RelatedModel
class Submodel1(models.Model):
related_model = models.OneToOneField(RelatedModel, null=True, blank=True, related_name='the_thing')
some_field = models.TextField()
# ...
class Submodel9(models.Model):
related_model = models.OneToOneField(RelatedModel, null=True, blank=True, related_name='the_thing')
another_field = models.TextField()
Это позволит вам получить доступ SubmodelX
из экземпляра RelatedModel
используя поле с именем the_thing
так же, как в примере с многостоловым наследованием, который вы сначала дали.
Обратите внимание, что вы можете использовать абстрактное наследование, чтобы выделить related_model
поле и любые другие общие поля между SubModel1
в Submodel9
,
Причина использования многотабличного наследования неэффективна, потому что он генерирует дополнительную таблицу для базовой модели и, следовательно, дополнительные JOIN для доступа к этим полям. Использование родовых отношений будет более эффективным, если вы позже обнаружите, что вам нужно ForeignKey
поле из RelatedModel
для каждого SubmodelX
, Однако Django не поддерживает родовые отношения в select_related()
и вам, возможно, придется в конечном итоге создать свои собственные запросы, чтобы сделать это эффективно. Компромисс между производительностью и простотой кодирования зависит от вас, в зависимости от того, какую нагрузку вы ожидаете на сервере и сколько времени вы хотите потратить на оптимизацию.
Ли возникновение LEFT OUTER JOIN
Я не могу сказать, что это проблема сама по себе, но, в любом случае, может быть интересно отметить, в каких случаях эти внешние объединения действительно происходят.
Это наивная попытка проиллюстрировать вышесказанное, используя несколько примеров запросов.
Предположим, у нас есть некоторые модели, использующие наследование нескольких таблиц следующим образом:
from django.db import models
class Parent(models.Model):
parent_field = models.CharField(max_length=10)
class ChildOne(Parent):
child_one_field = models.CharField(max_length=10)
class ChildTwo(Parent):
child_two_field = models.CharField(max_length=10)
По умолчанию дочерние экземпляры получают parent_ptr
и родительские экземпляры могут обращаться к дочерним объектам (если они существуют), используя childone
или же childtwo
, Обратите внимание, что parent_ptr
представляет отношение один к одному, которое используется в качестве первичного ключа (фактические дочерние таблицы не имеют id
колонка).
Вот быстрый и грязный юнит-тест с некоторым наивным Django
примеры запросов, показывающие соответствующее количество вхождений INNER JOIN
а также OUTER JOIN
в SQL
:
import re
from django.test import TestCase
from inheritance.models import (Parent, ChildOne, ChildTwo)
def count_joins(query, inner_outer):
""" Count the occurrences of JOIN in the query """
return len(re.findall('{} join'.format(inner_outer), str(query).lower()))
class TestMultiTableInheritance(TestCase):
def test_queries(self):
# get children (with parent info)
query = ChildOne.objects.all().query
self.assertEqual(1, count_joins(query, 'inner'))
self.assertEqual(0, count_joins(query, 'outer'))
# get parents
query = Parent.objects.all().query
self.assertEqual(0, count_joins(query, 'inner'))
self.assertEqual(0, count_joins(query, 'outer'))
# filter children by parent field
query = ChildOne.objects.filter(parent_field=parent_value).query
self.assertEqual(1, count_joins(query, 'inner'))
self.assertEqual(0, count_joins(query, 'outer'))
# filter parents by child field
query = Parent.objects.filter(childone__child_one_field=child_value).query
self.assertEqual(1, count_joins(query, 'inner'))
self.assertEqual(0, count_joins(query, 'outer'))
# get child field values via parent
query = Parent.objects.values_list('childone__child_one_field').query
self.assertEqual(0, count_joins(query, 'inner'))
self.assertEqual(1, count_joins(query, 'outer'))
# get multiple child field values via parent
query = Parent.objects.values_list('childone__child_one_field',
'childtwo__child_two_field').query
self.assertEqual(0, count_joins(query, 'inner'))
self.assertEqual(2, count_joins(query, 'outer'))
# get child-two field value from child-one, through parent
query = ChildOne.objects.values_list('parent_ptr__childtwo__child_two_field').query
self.assertEqual(1, count_joins(query, 'inner'))
self.assertEqual(1, count_joins(query, 'outer'))
# get parent field value from parent, but through child
query = Parent.objects.values_list('childone__parent_field').query
self.assertEqual(0, count_joins(query, 'inner'))
self.assertEqual(2, count_joins(query, 'outer'))
# filter parents by parent field, but through child
query = Parent.objects.filter(childone__parent_field=parent_value).query
self.assertEqual(2, count_joins(query, 'inner'))
self.assertEqual(0, count_joins(query, 'outer'))
Обратите внимание, что не все эти запросы имеют смысл: они только для иллюстративных целей.
Также обратите внимание, что этот тестовый код не СУХОЙ, но это специально.
Django реализует наследование нескольких таблиц через автоматически созданный OneToOneField, как сказано в его документации. Поэтому либо используйте абстрактное наследование, либо я не думаю, что использование явного OneToOneFields или ForeignKeys делает какие-либо различия.