Подтвердить что ты не робот

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

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

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

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

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

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

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

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

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

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

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

Так что же не так с этим видом наследования в Джанго? Почему явные OneToOneFields лучше?

Насколько сильно страдает производительность от JOIN? Есть ли тесты, которые показывают разницу в производительности?

Разве select_related() позволяет нам контролировать, когда вызывается JOIN?


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

4b9b3361

Ответ 1

Во-первых, наследование не имеет естественного перевода в архитектуру реляционных баз данных (хорошо, я знаю, 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 для некоторых пользователей.

Заключение

Вы должны знать стоимость написания и обслуживания кода без наследования модели, а также стоимость оборудования для поддержки приложений наследования модели и действовать соответственно.

Ответ 2

Из того, что я понимаю, вы используете 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.

Причина использования многозадачного наследования неэффективна, потому что она генерирует дополнительную таблицу для базовой модели и, следовательно, дополнительные JOINs для доступа к этим полям. Использование общих отношений было бы более эффективным, если позже вы обнаружите, что вам нужно поле ForeignKey из RelatedModel для каждого SubmodelX. Однако Django не поддерживает общие отношения в select_related(), и вам, возможно, придется создавать собственные запросы, чтобы сделать это эффективно. Компромисс между производительностью и простотой кодирования зависит от того, сколько нагрузки вы ожидаете на сервере и сколько времени вы хотите потратить на оптимизацию.

Ответ 3

Мир изменился.

Первое, что нужно отметить, это то, что статья под названием Django gotcha: конкретное наследование составляла почти четыре года на момент запроса этого вопроса; в 2014 году. С тех пор как системы Django, так и RDBM прошли долгий путь (например, mysql 5.0 или 5.1 были широко используемыми версиями и 5.5 общая доступность оставалась еще на один месяц).

Совместим слева от меня, присоединяется к моему праву

Верно, что многостраничное наследование действительно приводит к дополнительным объединениям за кулисами большую часть времени. Но объединения не являются злыми. Стоит отметить, что в правильно нормированной базе данных вы почти всегда должны присоединиться, чтобы получить все необходимые данные. Когда используются правильные индексы, объединения не включают существенных штрафных санкций.

INNER JOIN vs LEFT OUTER JOIN

Это действительно относится к многофакторному наследованию, при других подходах это позволяет избежать дорогостоящего LEFT OUTER JOIN и делать INNER JOIN вместо или, возможно, подзапроса. Но с множественным наследованием таблицы вам отказывают в выборе

Ответ 4

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

Ответ 5

Я не могу сказать, является ли возникновение 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).

Вот быстрый и грязный unit тест с некоторыми наивными примерами запросов 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'))

Обратите внимание, что не все эти запросы имеют смысл: они только для иллюстративных целей.

Также обратите внимание, что этот тестовый код не СУХОЙ, но это специально.