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:
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.
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
Modèle de base de référence dans un autre modèle (doit utiliser GenericForeignKey ou une dépendance inverse);
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.
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 joins
pour 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.
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.
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).
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.
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é
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.
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.