web-dev-qa-db-fra.com

Devrais-je éviter l'héritage multi-tables (concret) dans Django de quelque façon que ce soit?

De nombreux développeurs expérimentés recommandent de ne pas utiliser l'héritage multi-table de Django en raison de ses performances médiocres:

  1. Django Gotcha: héritage concret par Jacob Kaplan-Moss , un contributeur clé de Django.

    Dans presque tous les cas, l'héritage abstrait est une meilleure approche à long terme. J’ai vu plus de quelques sites écrasés sous la charge introduite par l’héritage concret, j’ai donc fortement suggéré aux utilisateurs de Django d’aborder avec scepticisme toute utilisation de l’héritage concret.

  2. Deux scoops de Django par Daniel Greenfield ( @pydanny )

    L'héritage multi-tables, parfois appelé «héritage concret», est considéré par les auteurs et de nombreux autres développeurs comme une mauvaise chose. Nous vous recommandons fortement de ne pas l'utiliser.

    A tout prix, tout le monde devrait éviter les héritages multi-tables car cela ajoute à la confusion et à des frais généraux substantiels. Au lieu de l'héritage multi-tables, utilisez les OneToOneFields et ForeignKeys explicites entre les modèles afin de pouvoir contrôler le moment où les jointures sont effectuées.

Mais sans l'héritage multi-table, je ne peux pas facilement

  1. Modèle de base de référence dans un autre modèle (doit utiliser GenericForeignKey ou une dépendance inverse);

  2. Récupère toutes les instances du modèle de base .

    (n'hésitez pas à ajouter plus)

Alors, qu'est-ce qui ne va pas avec ce type d'héritage à Django? Pourquoi les OneToOneFields explicites sont-ils meilleurs?

À quel point la performance souffre-t-elle des jointures? Existe-t-il des points de repère montrant la différence de performance?

select_related() ne nous permet-il pas de contrôler le moment où les JOIN sont appelées?


J'ai déplacé des exemples concrets vers une question distincte puisque celle-ci est en train de devenir trop large, et j'ai ajouté une liste de raisons pour utiliser l'héritage multi-tables à la place.

33
utapyngo

Tout d'abord, l'héritage n'a pas de traduction naturelle en architecture de base de données relationnelle (ok, je sais, les objets de type Oracle et certains autres héritages de SGBDR, mais Django ne profite pas de cette fonctionnalité)

À ce stade, remarquez que Django génère de nouvelles tables pour les sous-classes et écrivez beaucoup de left joinspour extraire les données de cette 'sous-tables'. Et les jointures à gauche ne sont pas vos amis . Dans un scénario de haute performance, comme un système de jeu ou autre chose, évitez-le et résolvez l'héritage "à la main" à l'aide d'artéfaces telles que des clés nuls, OneToOne ou étrangères. Dans le scénario OneToOne, vous pouvez appeler la table associée directement ou uniquement si vous en avez besoin.

... MAIS ...

"À mon avis (TGW)" vous devez inclure l'héritage de modèle dans vos projets d'entreprise lorsqu'il est transmis à votre univers de discours. Je fais cela et je gagne beaucoup d’heures de développement pour mes clients grâce à cette fonctionnalité. Aussi le code devient propre et élégant et cela signifie une maintenance facile (sachez que ce type de projet ne comporte pas des centaines de requêtes ni des requêtes à la seconde)

Question par question

Q: Qu'est-ce qui ne va pas avec ce type d'héritage à Django?
A: Beaucoup de tables, beaucoup de jointures à gauche.

Q: Pourquoi les OneToOneFields explicites sont-ils meilleurs?
R: Vous pouvez accéder directement au modèle associé sans jointure à gauche.

Q: Existe-t-il des exemples illustratifs (points de repère)?
R: Non comparable.

Q: Est-ce que select_related () ne nous permet pas de contrôler le moment où les JOIN sont appelées?
A: Django joint les tables nécessaires.

Q: Quelles sont les alternatives à l'héritage multi-table lorsque je dois référencer une classe de base dans un autre modèle?
R: Annulation. OneToOne relations et beaucoup de lignes de code. Cela dépend des besoins de l'application.

Q: GenericForeignKeys est-il meilleur dans ce cas?
R: Non pour moi.

Q: Que faire si j'ai besoin de OneToOneField pour baser le modèle? A: écris-le. Cela ne pose aucun problème. Par exemple, vous pouvez étendre le modèle d’utilisateur et avoir un modèle de base OneToOne à utilisateur pour certains utilisateurs.

Conclusion

Vous devez connaître le coût du code d'écriture et de maintenance sans héritage de modèle, ainsi que le coût du matériel nécessaire à la prise en charge des applications d'héritage de modèle et agir en conséquence.

22
dani herrera

D'après ce que j'ai compris, vous utilisez OneToOneField de RelatedModel à BaseModel car vous souhaitez en fin de compte créer un lien un-à-un entre RelatedModel et chaque Submodel1 à Submodel9. Si tel est le cas, il existe un moyen plus efficace de le faire sans héritage multi-table ni relations génériques.

Supprimez simplement BaseModel et dans chaque SubmodelX, ayez un 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()

Cela vous permettrait d'accéder à SubmodelX à partir d'une instance de RelatedModel à l'aide d'un champ nommé the_thing, comme dans l'exemple d'héritage multi-table que vous avez donné en premier.

Notez que vous pouvez utiliser l'héritage abstrait pour décomposer le champ related_model et tout autre champ commun compris entre SubModel1 et Submodel9.

L'utilisation de l'héritage multi-table est inefficace, car elle génère une table supplémentaire pour le modèle de base et, par conséquent, des JOIN supplémentaires pour accéder à ces champs. L'utilisation de relations génériques serait plus efficace si, par la suite, vous aviez besoin d'un champ ForeignKey de RelatedModel à chaque SubmodelX. Cependant, Django ne prend pas en charge les relations génériques dans select_related() et vous devrez peut-être créer vos propres requêtes pour le faire efficacement. Le compromis entre performance et facilité de codage dépend de la charge attendue sur le serveur et du temps que vous souhaitez consacrer à l'optimisation.

11
user193130

Le monde a changé.

La première chose à noter est que l'article intitulé Django Gotcha: héritage concret avait presque quatre ans au moment où cette question a été posée; en 2014. Les deux systèmes Django et RDBM ont parcouru un long chemin depuis lors (par exemple, mysql 5.0 ou 5.1 étaient les versions largement utilisées et la disponibilité générale de la version 5.5 était encore dans un mois).

Se joint à ma gauche, se joint à ma droite

Il est vrai que l'héritage multi-tables entraîne des jointures supplémentaires en arrière-plan la plupart du temps . Mais les jointures ne sont pas mauvaises. Il est à noter que dans une base de données correctement normalisée, vous devez presque toujours vous joindre pour récupérer toutes les données requises. Lorsque des index appropriés sont utilisés, les jointures n'incluent pas de pénalité de performances significative.

INNER JOIN vs LEFT OUTER JOIN

C'est en effet le cas contre l'héritage multi-table, avec d'autres approches, il est possible d'éviter un coûtant LEFT OUTER JOIN et d'effectuer un INNER JOIN à la place ou peut-être une sous-requête. Mais avec l'héritage multi-tables, ce choix vous est refusé

5
e4c5

Django implémente l'héritage multi-tables via un OneToOneField créé automatiquement, comme le dit sa documentation. Utilisez l'héritage abstrait ou je ne pense pas que l'utilisation d'un OneToOneFields ou d'un ForeignKeys explicite fasse la différence.

0
youngjack

Je ne saurais dire si l'occurrence de LEFT OUTER JOIN est un problème en soi, mais, dans tous les cas, il peut être intéressant de noter dans quels cas ces jointures externes se produisent réellement .

Ceci est une tentative naïve d’illustrer ce qui précède, en utilisant quelques exemples de requêtes.

Supposons que certains modèles utilisent l'héritage multi-table comme suit:

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)

Par défaut, les instances enfant obtiennent un parent_ptr et les instances parent peuvent accéder aux objets enfant (s'ils existent) à l'aide de childone ou childtwo. Notez que parent_ptr représente une relation un-à-un qui est utilisée comme clé primaire (les tables enfants réelles n'ont pas de colonne id).

Voici un test unitaire rapide avec quelques exemples de requête naïfs Django, montrant le nombre correspondant d'occurrences de INNER JOIN et OUTER JOIN dans la 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'))

Notez que toutes ces requêtes n’ont pas de sens: elles ne servent qu’à des fins d’illustration.

Notez également que ce code de test n'est pas DRY, mais que c'est intentionnel.

0
djvg