Relaciones many to many: conceptos y fundamentos.
Published: 2024-05-17 03:16:39



Al crear una aplicación con Django u otro framework, a menudo te encuentras con situaciones en las que necesitas establecer relaciones entre modelos. Un tipo común de relación es la relación de muchos a muchos
(M2M).
En este artículo, exploraremos qué son las relaciones M2M, cuándo usarlas y cómo implementarlas en
Django y Django Ninja.

¿Qué son las relaciones de muchos a muchos?

En una relación tradicional uno a uno (1:1) o uno a muchos (1:N), cada registro de una tabla corresponde
exactamente a un registro de otra tabla. Por el contrario, una relación de muchos a muchos (M2M) es un
tipo de relación en la que un registro de una tabla se puede asociar con varios registros de otra tabla y
viceversa. A esto se le suele denominar tabla de "puente" o "unión".

Cuándo utilizar relaciones de muchos a muchos

Hay varios escenarios en los que es posible que desee utilizar relaciones M:N

1. Etiquetado o categorización : cuando tienes un modelo que se puede asociar con múltiples categorías o
   etiquetas, como una publicación de blog que se puede etiquetar con múltiples temas.

2. Roles o permisos de usuario : cuando necesita asignar múltiples roles o permisos a un usuario, como
   administrador, moderador o colaborador.

3. Asociaciones de muchos a muchos : cuando necesita establecer relaciones entre múltiples modelos,
   como un libro que se puede asociar con múltiples autores y múltiples géneros.

Cómo implementar relaciones de muchos a muchos utilizando Django?

Para implementar una relación M:N en Django, necesitarás crear dos modelos y luego usar la clase
ManyToManyField en el archivo de modelos.

He aquí un ejemplo:

```py
# models.py

from django.db import models
from django.utils.text import slugify
from django.contrib.auth.models import User


class Category(models.Model):
    name = models.CharField(max_length=50)

    def __str__(self):
        # return self.name
        return self.name


class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    slug = models.SlugField(blank=True)
    image_src = models.SlugField(blank=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    categories = models.ManyToManyField(Category, blank=True)

    def save(self, *args, **kwargs):
        words = self.title.split()  # Split the title into words
        first_two_words = " ".join(words[:2])  # Select the first two words
        formatted_title = first_two_words.replace(" ", "_")  # Replace spaces with
        self.slug = slugify(self.title.replace(" ", "-"))
        self.image_src = slugify(formatted_title)
        super().save(*args, **kwargs)

    def __str__(self):
        return self.title

```



En este caso, Django creará automáticamente una tabla intermedia que se encargará de almacenar las
relaciones entre los usuarios y sus roles. Podemos entonces acceder a las relaciones utilizando el
método categories en el modelo Post.

Al utilizar la clase ManyToManyField evita la creación de una tabla intermedia adicional, lo que puede mejorar el rendimiento y la escalabilidad.

Proporciona una forma más fácil y concisa de definir relaciones muchos a muchos.
Permite acceder a las relaciones utilizando métodos naturales o (built-in) en los modelos.

En resumen, la elección entre utilizar el mecanismo automático de Django o crear una tabla intermedia
manualmente depende del proyecto y las necesidades específicas. Es importante considerar las ventajas y
desventajas de cada opción antes de tomar una decisión.
Siempre que sea posible es más simple utilizar ManyToManyField en lugar de una tabla intermedia cuando se trata de una relación muchos a muchos en Django.

https://docs.djangoproject.com/en/3.2/ref/models/fields/#manytomanyfield

Ejemplo de creación de una tabla intermedia:
```py
# models.py

from django.db import models


class IntermediaryModel(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    is_released = models.BooleanField()
    #...

```

Ya con los modelos creados podemos utilizarlos para crear registros
en nuestra base de datos e implementar una API en este caso con Django
Ninja veamos un ejemplo:

Primero creamos los esquemas en un archivo que convencionalmente se llama
schemas.py

```py
from ninja import ModelSchema, Schema
from posts.models import Post, Category
from django.contrib.auth.models import User
from datetime import datetime


class ErrorMessage(Schema):
    message: str


class UserSchema(ModelSchema):
    class Meta:
        model = User
        fields = ["id", "username"]

    # exclude = ["last_login", "user_permissions"]


class CategorySchema(ModelSchema):
    class Meta:
        model = Category
        fields = ["id", "name"]


class PostSchema(ModelSchema):
    author: UserSchema
    categories: CategorySchema

    class Meta:
        model = Post
        fields = ["id", "title", "slug", "content", "categories"]


class CategoryFullSchema(Schema):
    id: int
    name: str


class PostFullSchema(Schema):
    id: int
    title: str
    slug: str
    image_src: str
    content: str
    author: UserSchema
    categories: list[CategoryFullSchema]
    created_at: datetime = datetime.now()


class PostUpdateSchema(Schema):
    title: str
    content: str


class PostCreateSchema(Schema):
    title: str
    content: str
    author_id: int = None
    categories: list[str]

```


Luego de crear los esquemas entonces creamos otro archivo que suele llamarse
api.py


```py
from ninja import Router, NinjaAPI
from ninja.responses import Response
from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User
from posts.models import Post, Category
from ninja.security import HttpBearer
from django.conf import settings
from posts.schemas import PostSchema, PostUpdateSchema, UserSchema
from posts.schemas import PostFullSchema, PostCreateSchema
from posts.schemas import CategorySchema, CategoryFullSchema
from posts.schemas import ErrorMessage


@router.get("/get/single/post/{post_id}", response={200: PostFullSchema})
def get_post(request, post_id: int):
    try:
        post = Post.objects.get(id=post_id)
        return post
    except Post.DoesNotExist:
        raise HttpError(404, "Not found!.")


@router.get("/postlist/", response={200: list[PostFullSchema]})
def get_all_posts(request):
    print(Post.objects.all())
    return Post.objects.all()


@router.post("/create/post-category", response={200: PostFullSchema, 400: ErrorMessage})
def create_post_and_category(request, payload: PostCreateSchema):
    categories = []

    try:
        if Post.objects.filter(title=payload.title).exists():
            return 400, {"message": "title already exists"}
    except:
        raise HttpError(400, {"message": "title already exists"})

    for category_name in payload.categories:
        if not Category.objects.filter(name=category_name).exists():
            Category.objects.create(name=category_name)
        else:
            category = Category.objects.get(name=category_name)
            categories.append(category)

    post = Post.objects.create(
        title=payload.title, content=payload.content, author=User(pk=payload.author_id)
    )
    post.categories.add(*categories)

    return post

```

Puede tomar como punto de partida el ejemplo anterior y ajustarlo
a sus necesidades.

En el mundo de la programación, las relaciones entre objetos son fundamentales para representar y
manipular datos. En este sentido, las relaciones muchos a muchos (M:N) son especialmente útiles cuando se
necesitan representar conexiones entre objetos que pueden tener múltiples asociaciones

En conclusión, las relaciones de muchos a muchos son una herramienta poderosa que permite establecer relaciones complejas entre modelos. Si comprende cuándo utilizarlos y cómo implementarlos, podrá crear aplicaciones más sólidas y escalables.

Espero que este artículo haya sido útil. Si tiene alguna pregunta o desea obtener más información sobre este tema, ¡no dude en preguntar!