Relaciones polimórficas con Django



@eduardo_gpg

Número de visitas 2492

Tiempo de lectura 6 min

12 Noviembre 2021

Sin duda, uno de los temas más interesantes con el cual podemos llegar a toparnos, al momento de trabajar con algún tipo ORM, es el tema de relaciones polimórficas.

Si nunca habías escuchado acerca de este termino, bueno, no te preocupes, aquí te lo explico.

Para comprender bien el tema de relaciones polimórficas sin duda debemos recordad el concepto de Polimorfismo que, en términos simples, es la capacidad de un objeto de poder tomar diferentes formas y así adoptar comportamientos diferentes.

Un ejemplo muy común es el de la clase Figura Geométricas, donde un objeto de este tipo puede tomar múltiples formas, ya sea un cuadrado, triangulo, circulo etc.. Dependiendo de la forma que tome será los comportamientos y atributos que poseerá.

Por lo tanto una relación polimórfica no será más que una relación donde los objetos relacionados podrán ser de diferentes tipos. Es decir, podrán tomar diferentes formar.

Al nosotros utilizar relaciones polimórficas podremos trabajar con una N cantidad de modelos, pudiendo así crear relaciones mucho más flexibles que con las clásicas relaciones uno a uno, uno a mucho o mucho a muchos.

Es por ello que, en esta ocasión, me gustaría traer un nuevo tutorial explicando en detalle como podemos implementar las relaciones polimórficas con Django. Es un tema muy interesante, y si eres un/una desarrolladora Python y te interesa o ya te encuentras trabajando en el desarrollo web, este post sin duda puede serte de mucho utilidad, así que te invito a que te quedes.

Bien, sin más introducción, comencemos con el post.

Modelos

Para este post estaré trabajando con 4 modelos de Django. Los cuales serán los siguientes. 🤠

  • Video
  • Article
  • Item
  • Course

Las relaciones entre estos modelos será la siguiente:

Un curso puede poseer múltiples items; donde un item puede ser de tipo video o artículo. En otras palabras , un curso puede poseer múltiples vídeos o artículos.

La relación será polimórfica, uno a mucho, donde el mucho pueden ser de múltiples tipos. Dejaremos que sean los objetos de tipo item quienes decida el tipo; ya sea artículo o vídeo.

Recordemos: Un curso puede poseer múltiples items, que estos pueden ser de tipo artículo o vídeo.

Para este tutorial con 2 modelos polimórficos será más que suficiente, sin embargo tú puedes tener la N cantidad de modelos relacionados que desees. Por ejemplo, un curso pudiera tener múltiples vídeos, artículos, ejercicios, challenges, tips etc... 😁 No hay una cantidad limitada.

Bien. Creo que la explicación esta clara, ahora pasemos a los modelos. Los modelos quedarían de la siguiente manera.

Course

from django.db import models

class Course(models.Model):
    title = models.CharField(max_length=50)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Article

from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=50)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Video

from django.db import models

class Video(models.Model):
    title = models.CharField(max_length=50)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Como puedes observar modelos bastante sencillos, pero para esta ocasión funcionarán bástate bien. 😎

Relación polimórfica 🕵️‍♀️

Para que nosotros podamos implementar relaciones polimórficas en Django haremos uso de relaciones genericas. Sí así como lo lees, relaciones genericas. Ahora le indicaremos a nuestros objetos que poseerá una relación con otros objetos, pero no le indicaremos de que tipos serán. 🤯

Para lograr esto nos apoyaremos de las clase GenericForeignKey y ContentType.

Nuestro modelos Item quedaría de la siguiente manera.

from django.db import models

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType

from courses.models import Course

class Item(models.Model):
    course = models.ForeignKey(Course, on_delete=models.CASCADE)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey()

    created_at = models.DateTimeField(auto_now_add=True)

Como podemos observar nuestro modelo tiene una relación con curso, ya que un item le pertenece a un curso. Hasta aquí todo bien.

Lo interesante es observar los 3 nuevos atributos que posee el modelo.

  • content_type Tipo de objeto relacionado.
  • object_id Id del objeto relacionado.
  • content_object El objeto relacionado.

Mediante estos 3 atributos seremos capaces de establecer una relación genérica con prácticamente cualquier modelo de nuestra aplicación.

Si bien los 3 atributos son de suma importancia, será mediante el atributo content_object con el cual seremos capaces tanto de establecer como de obtener la relación polimórfica.

Para que la relación sea bi-direccional debemos añadir un nuevo atributo de tipo GenericRelation a nuestro modelos a relacionar.

Article

from django.db import models
from django.contrib.contenttypes.fields import GenericRelation

from items.models import Item

class Article(models.Model):
    title = models.CharField(max_length=50)
    item = GenericRelation(Item)

    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Video

from django.db import models
from django.contrib.contenttypes.fields import GenericRelation

from items.models import Item

class Video(models.Model):
    title = models.CharField(max_length=50)
    item = GenericRelation(Item)

    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Mediante el atributo item (GenericRelation) tanto los vídeos como los artículos podrán acceder al objeto relacionado (Item) que a su vez permitirá acceder al curso.

  • Article -> Item -> Course
  • Video -> Item -> Course

Un vídeo o artículo poseerá un objeto de tipo item, y un item podrá ser de tipo vídeo o artículo.

Se que esto puede sonar algo complejo, así que pongamos todo esto a prueba.

Primero realicemos las migraciones correspondientes.

python mananage.py makemigrations
python mananage.py migrate

Perfecto, ahora sí, una vez hemos definido las relaciones genéricas ya seremos capaces de implementar el tema de relaciones polimórficas de forma correcta. Para probar esto, creemos un par de objetos, un curso y un par de artículos y vídeos.

>>> from courses.models import Course

>>> course = Course.objects.create(title='Curso profesional de Python')
>>> course
<Course: Curso profesional de Python>
>>> from articles.models import Article

>>> article_1 = Article.objects.create(title='Introducción al curso')
>>> article_2 = Article.objects.create(title='Presentación del tutor')
>>> from videos.models import Video

>>> video_1 = Video.objects.create(title='Temario del curso')
>>> video_2 = Video.objects.create(title='Hola mundo')

Listo, una vez con los objetos creados, el siguiente paso será relacionarlos entre sí, y para ello haremos uso del modelo Item, modelo que será el puente entre un curso y sus elementos (Para este ejemplo artículos o videos).

from items.models import Item

Item.objects.create(course=course, content_object=article_1)
<Item: Item object (1)>

Item.objects.create(course=course, content_object=article_2)
<Item: Item object (2)>

Item.objects.create(course=course, content_object=video_1)
<Item: Item object (3)>

Item.objects.create(course=course, content_object=video_2)
<Item: Item object (4)>

Para crear un objeto de tipo item hay que pasar por valores tanto el curso al cual pertenece nuestro objeto como el objeto relacionado (artículo o vídeo).

Será mediante el atributo content_object que seremos capaces de establecer la relación polimórfica entre los modelos, pudiendo así nuestro objeto de tipo item tomar diferentes "formas".

Una vez se establezca un valor para el atributo content_object los valores para los atributos content_type y content_id serán asignados. Recordemos que estos atributos almacenan, respectivamente, el tipo de objeto y su id.

Comprenderemos mejor esto si inspeccionamos en detalle el objeto.

>>> item = Item.objects.last()

>>> item.content_type 
<ContentType: video | video>

>>> item.object_id
2

>>> item.content_object
<Video: Hola mundo>

Podemos confirmar, se almacena tanto el id del objeto relacionado como su tipo.

Seremos capaces de acceder al objeto per se utilizando el atributo content_object. 🤓

>>> item = Item.objects.first()

>>> item.content_type 
<ContentType: article | article>

>>> item.object_id
1

>>> item.content_object
<Article: Introducción al curso>

Cool, con todo esto, ahora nuestros cursos podrá poseer múltiples items de diferentes tipos.

Objetos relacionados

Listo, una vez hayamos definido el modelo Polimórfico ¿Qué pasa con los objetos relacionado? Bueno, realmente no mucho. Podremos acceder al objeto polimófico mediante los objetos de tipo GenericRelation.

Recordemos que anteriormente hemos definido el atributo item tanto en la clase Video como en Article.

>>> article = Article.objects.first()

>>> article.item.first()
<Item: Item object (1)>
>>> video = Video.objects.first()

>>> video.item.first()
<Item: Item object (3)>

En este caso el objeto item no es más que un listado de items (Por esto podemos utilizar el método first), que si bien en otros escenarios puede sernos de mucha utilidad, para este ejemplo en concreto no es así. Por lo tanto vamos a modificar nuestros modelos para que podamos acceder tanto al objeto item como al curso.

Para la clase Video quedaría así.

class Video(models.Model):
    title = models.CharField(max_length=50)
    items = GenericRelation(Item)

    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

    @property
    def item(self):
        if self.items:
            return self.items.first()

        return None

    @property
    def course(self):
        if self.item:
            return self.item.course

        return None

Añadimos 2 nuevos métodos y renombramos el atributo item por items. ¿Queda mucho mejor no lo crees? 🤔

Para la clase Article sería exactamente lo mismo. Sí lo sé, habrá codigo duplicado, pero eso ya lo estaremos resolviendo en otro post, con modelos abstractos.

Y listo, el uso de estos objetos ahora es mucho más sencillo.

>>> video = Video.objects.last()

>>> video.item
<Item: Item object (3)>

>>> video.course
<Course: Curso profesional de Python>

Conclusión

Como pudimos observar las relaciones polimórficas nos permite trabajar con una mayor cantidad de modelos, pudiendo así crear relaciones mucho más sencillas y sobre todo flexible.

Si bien existen múltiples formas de implementar este tipo de relaciones (Múltiples llaves foráneas, Enums etc...) La forma correcta de hacerlo siempre será mediante relaciones genericas.

Si te interesa profundizar más en este tema te comparto el link a la documentación oficial de Django. Espero puedas echarle un vistazo.

Y bien, creo que con esto estaríamos finalizando el post en esta ocasión. Espero el post haya sido de tu agrado y si es así, no dude es hacérnoslo saber en la sección de comentarios.

Sin más que decir nos leemos en una siguiente entrega. 🍻


¿El contenido te resulto de ayuda?

Para poder dejar tu opinión es necesario ser un usuario autenticado. Login

Más Tips y Ejercicios 🐍

Adquiere una subscripción PyWombat por tan solo $3 USD. al mes.

Conoce los beneficios de ser usuario premium:
Niveles desbloqueados: Ten accesos a todos los niveles de ejercicios. 🔓
Nuevo límite: Incrementa tu límite de ejercicios por semana. 🚀
Contenido único: Recibe semanalmente recursos exclusivos de Python (Videos, Artículos y Capitulos del libro PyWombat, comienza como desarrollador Python. 🐍