Alternativas de herencia de tablas múltiples de Django para un patrón de modelo de datos básico

tl; dr

¿Existe una alternativa simple a la herencia de múltiples tablas para implementar el patrón básico de modelo de datos que se muestra a continuación, en Django?

Premisa

Considere el patrón de modelo de datos muy básico en la imagen a continuación, basado en, por ejemplo , Hay, 1996 .

En pocas palabras: las Organizations y las Persons son Parties , y todas las Parties tienen Address . Un patrón similar puede aplicarse a muchas otras situaciones.

El punto importante aquí es que la Address tiene una relación explícita con la Party , en lugar de relaciones explícitas con los submodelos individuales Organization y Person .

diagrama que muestra el modelo básico de datos

Tenga en cuenta que cada submodelo introduce campos adicionales (no se muestran aquí, pero vea el ejemplo de código a continuación).

Este ejemplo específico tiene varios defectos obvios, pero eso no viene al caso. Por el bien de esta discusión, supongamos que el patrón describe perfectamente lo que deseamos lograr, por lo que la única pregunta que queda es cómo implementar el patrón en Django .

Implementación

La implementación más obvia, creo, usaría la herencia de tablas múltiples :

 class Party(models.Model): """ Note this is a concrete model, not an abstract one. """ name = models.CharField(max_length=20) class Organization(Party): """ Note that a one-to-one relation 'party_ptr' is automatically added, and this is used as the primary key (the actual table has no 'id' column). The same holds for Person. """ type = models.CharField(max_length=20) class Person(Party): favorite_color = models.CharField(max_length=20) class Address(models.Model): """ Note that, because Party is a concrete model, rather than an abstract one, we can reference it directly in a foreign key. Since the Person and Organization models have one-to-one relations with Party which act as primary key, we can conveniently create Address objects setting either party=party_instance, party=organization_instance, or party=person_instance. """ party = models.ForeignKey(to=Party, on_delete=models.CASCADE) 

Esto parece coincidir perfectamente con el patrón. Casi me hace creer que esto es a lo que se destinaba la herencia de tablas múltiples en primer lugar.

Sin embargo, la herencia de varias tablas parece estar mal vista , especialmente desde un punto de vista de rendimiento, aunque depende de la aplicación . Especialmente esta publicación aterradora, pero antigua , de uno de los creadores de Django es bastante desalentadora:

En casi todos los casos, la herencia abstracta es un mejor enfoque a largo plazo. He visto más de unos pocos sitios aplastados bajo la carga introducida por la herencia concreta, por lo que sugiero que los usuarios de Django se acerquen a cualquier uso de la herencia concreta con una gran dosis de escepticismo.

A pesar de esta advertencia de miedo, supongo que el punto principal en esa publicación es la siguiente observación con respecto a la herencia de tablas múltiples:

Estas uniones tienden a estar “ocultas”, se crean automáticamente, y significan que lo que parecen consultas simples a menudo no lo son.

Desambiguación : la publicación anterior se refiere a la “herencia de múltiples tablas” de Django como “herencia concreta”, que no debe confundirse con la herencia de tablas concretas en el nivel de la base de datos. Este último en realidad se corresponde mejor con la noción de herencia de Django usando clases base abstractas.

Supongo que esta pregunta SO ilustra muy bien el problema de las “uniones ocultas”.

Alternativas

La herencia abstracta no me parece una alternativa viable, porque no podemos establecer una clave externa para un modelo abstracto, lo que tiene sentido, porque no tiene tabla. Supongo que esto implica que necesitaríamos una clave externa para cada modelo “secundario” más alguna lógica adicional para simular esto.

La herencia de proxy tampoco parece ser una opción, ya que los submodelos introducen campos adicionales. EDITAR: Pensándolo bien , los modelos proxy podrían ser una opción si usamos la herencia de una sola tabla en el nivel de la base de datos, es decir, usamos una sola tabla que incluye todos los campos de Party , Organization y Person .

Las relaciones GenericForeignKey pueden ser una opción en algunos casos específicos , pero para mí son la materia de las pesadillas.

Como otra alternativa, a menudo se sugiere utilizar relaciones explícitas de uno a uno ( eoto para abreviar, aquí) en lugar de la herencia de tablas múltiples (de modo que Party , Person y Organization solo serían subclases de models.Model ).

Ambos enfoques, la herencia de tablas múltiples ( mti ) y las relaciones explícitas uno a uno ( eoto ), dan como resultado tres tablas de base de datos. Entonces, dependiendo del tipo de consulta, por supuesto , alguna forma de JOIN es a menudo inevitable cuando se recuperan datos.

Al inspeccionar las tablas resultantes en la base de datos, queda claro que la única diferencia entre los enfoques mti y eoto , en el nivel de la base de datos, es que una tabla eoto Person tiene una columna id como clave principal y una columna separada de clave externa. a Party.id , mientras que una tabla mti Person no tiene una columna de id separada, sino que utiliza la clave Party.id de Party.id como su clave principal.

Pregunta (s)

No creo que el comportamiento del ejemplo (especialmente la relación directa única con el padre) pueda lograrse con una herencia abstracta , ¿verdad? Si puede , entonces, ¿cómo lograrías eso?

¿Es realmente una relación de uno a uno explícita mucho mejor que la herencia de tablas múltiples, excepto por el hecho de que nos obliga a hacer que nuestras consultas sean más explícitas? Para mí, la conveniencia y claridad del enfoque de múltiples mesas supera el argumento explícito.

Tenga en cuenta que esta pregunta SO es muy similar, pero no responde a mis preguntas. Además, la última respuesta ahora tiene casi nueve años , y Django ha cambiado mucho desde entonces.

[1]: Hay 1996, Data Model Patterns

Mientras espero una mejor, aquí está mi bash de una respuesta.

Como lo sugirió Kevin Christopher Henry en los comentarios anteriores, tiene sentido abordar el problema desde el lado de la base de datos. Como mi experiencia con el diseño de bases de datos es limitada, debo confiar en otros para esta parte.

Por favor, corrígeme si me equivoco en cualquier momento.

Base de datos del modelo de datos frente a la aplicación (orientada a objetos) frente a (relacional)

Se puede decir mucho sobre la discrepancia entre el objeto y la relación o, más precisamente, sobre la falta de coincidencia entre el modelo de datos / objeto y la relación.

En el contexto actual, creo que es importante tener en cuenta que una traducción directa entre el modelo de datos , la implementación orientada a objetos (Django) y la implementación de bases de datos relacionales , no siempre es posible o incluso deseable. Un bonito diagtwig de Venn de tres vías probablemente podría ilustrar esto.

Nivel de modelo de datos

Para mí, un modelo de datos como se ilustra en la publicación original representa un bash de capturar la esencia de un sistema de información del mundo real. Debe ser lo suficientemente detallado y flexible para permitirnos alcanzar nuestra meta. No prescribe los detalles de la implementación, pero puede limitar nuestras opciones de todas formas.

En este caso, la herencia plantea un desafío principalmente en el nivel de implementación de la base de datos.

Nivel de base de datos relacional

Algunas respuestas de SO que tratan con implementaciones de bases de datos de herencia (única) son:

  • ¿Cómo puedes representar la herencia en una base de datos?

  • ¿Cómo modelar efectivamente la herencia en una base de datos?

  • ¿Técnicas para la herencia de bases de datos?

Todos estos más o menos siguen los patrones descritos en el libro de Martin Fowler Patrones de architecture de aplicaciones . Hasta que llegue una mejor respuesta, me inclino a confiar en estas opiniones. La sección de herencia en el capítulo 3 (edición de 2011) lo resume muy bien:

Para cualquier estructura de herencia hay básicamente tres opciones. Puede tener una tabla para todas las clases en la jerarquía: Herencia de tabla única (278) …; Una mesa para cada clase de concreto: Herencia de la mesa de concreto (293) …; o una tabla por clase en la jerarquía: herencia de tabla de clase (285) …

y

Las compensaciones son todas entre la duplicación de la estructura de datos y la velocidad de acceso. … No hay un ganador claro aquí. … Mi primera elección tiende a ser la herencia de una sola mesa

Un resumen de los patrones del libro se encuentra en martinfowler.com .

Nivel de aplicación

La API de mapeo relacional de objetos (ORM) de Django nos permite implementar estos tres enfoques, aunque el mapeo no es estrictamente uno a uno.

Los documentos de herencia del modelo Django distinguen tres “estilos de herencia”, según el tipo de clase de modelo utilizado ( concreto , abstracto , proxy ):

  1. resumen padre con hijos concretos ( clases base abstractas ): la clase padre no tiene tabla de base de datos. En su lugar, cada clase secundaria tiene su propia tabla de base de datos con sus propios campos y duplicados de los campos principales. Esto se parece mucho a la herencia de tablas concretas en la base de datos.

  2. padre concreto con hijos concretos ( herencia de varias tablas ): la clase padre tiene una tabla de base de datos con sus propios campos, y cada clase hijo tiene su propia tabla con sus propios campos y una clave externa (como clave principal) para el padre mesa. Esto se parece a la herencia de tabla de clase en la base de datos.

  3. padre concreto con hijos proxy ( modelos proxy ): la clase padre tiene una tabla de base de datos, pero los hijos no lo hacen . En su lugar, las clases secundarias interactúan directamente con la tabla principal. Ahora, si agregamos todos los campos de los hijos (como se define en nuestro modelo de datos) a la clase principal , esto podría interpretarse como una implementación de la herencia de una sola tabla . Los modelos de proxy proporcionan una forma conveniente de tratar el lado de la aplicación de la tabla de base de datos grande y única.

Conclusión

Me parece que, para el ejemplo actual, la combinación de la herencia de una sola tabla con los modelos de proxy de Django puede ser una buena solución que no tiene las desventajas de las combinaciones “ocultas”.

Aplicado al ejemplo de la publicación original, se vería algo así:

 class Party(models.Model): """ All the fields from the hierarchy are on this class """ name = models.CharField(max_length=20) type = models.CharField(max_length=20) favorite_color = models.CharField(max_length=20) class Organization(Party): class Meta: """ A proxy has no database table (it uses the parent's table) """ proxy = True def __str__(self): """ We can do subclass-specific stuff on the proxies """ return '{} is a {}'.format(self.name, self.type) class Person(Party): class Meta: proxy = True def __str__(self): return '{} likes {}'.format(self.name, self.favorite_color) class Address(models.Model): """ As required, we can link to Party, but we can set the field using either party=person_instance, party=organization_instance, or party=party_instance """ party = models.ForeignKey(to=Party, on_delete=models.CASCADE)