Должен ли я каким-либо образом избегать многотабличного (конкретного) наследования в Django?

Многие опытные разработчики рекомендуют не использовать наследование нескольких таблиц Django из-за его низкой производительности:

  1. Джанго Гоча: конкретное наследство от Якоба Каплана-Мосса, основного участника Джанго.

    Почти в каждом случае абстрактное наследование является лучшим подходом в долгосрочной перспективе. Я видел более чем несколько сайтов, разрушенных под нагрузкой, вызванной конкретным наследованием, поэтому я настоятельно рекомендую пользователям Django подходить к любому использованию конкретного наследования с большой долей скептицизма.

  2. Два совка Джанго от Дэниела Гринфилда ( pydanny)

    Многостоловое наследование, иногда называемое "конкретным наследованием", считается авторами и многими другими разработчиками плохой вещью. Мы настоятельно рекомендуем не использовать его.

    Любой ценой каждый должен избегать наследования нескольких таблиц, так как это добавляет путаницу и существенные накладные расходы. Вместо многотабличного наследования используйте явные OneToOneFields и ForeignKeys между моделями, чтобы вы могли контролировать прохождение соединений.

Но без наследования нескольких таблиц я не могу легко

  1. Ссылочная базовая модель в другой модели (необходимо использовать GenericForeignKey или обратную зависимость);

  2. Получить все экземпляры базовой модели.

    (не стесняйтесь добавлять больше)

Так что же не так с этим видом наследования в Джанго? Почему явные 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 делает какие-либо различия.

Другие вопросы по тегам