Many to Many Django

Pour définit une relation ManyToMany dans votre modèle, vous pouvez utiliser le champ models.ManyToManyField

ManyToMany c'est quoi au juste?

Many-to-many signifique littéralement "Plusieurs à Plusieurs". C'est un concept qui permet de lier plusieurs items à plusieurs endroits. C'est une sorte de clé étrangère qui peut contenir plusieurs items.

Exemple de many to many

Passons à la pratique en gardant notre exemple d'eboutique:

backoffice/models.py

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from django.db import models

class Product(models.Model):
    """
    Produit : prix, code, etc.
    """
    
    class Meta:
        verbose_name = "Produit"
        
    name       = models.CharField(max_length=100)
    code       = models.CharField(max_length=10, null=True, blank=True, unique=True)
    price_ht   = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="Prix unitaire HT")
    price_ttc  = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="Prix unitaire TTC")
    
    def __unicode__(self):
        return u"{0} [{1}]".format(self.name, self.code)

class ProductItem(models.Model):
    """
    Déclinaison de produit déterminée par des attributs comme la couleur, etc.
    """
    
    class Meta:
        verbose_name = "Déclinaison Produit"
        
    product     = models.ForeignKey('Product')
    code        = models.CharField(max_length=10, null=True, blank=True, unique=True)
    code_ean13  = models.CharField(max_length=13)
    attributes  = models.ManyToManyField("ProductAttributeValue", related_name="product_item", null=True, blank=True)
       
    def __unicode__(self):
        return u"{0} [{1}]".format(self.product.name, self.code)
    
class ProductAttribute(models.Model):
    """
    Attributs produit
    """
    
    class Meta:
        verbose_name = "Attribut"
        
    name =  models.CharField(max_length=100)
    
    def __unicode__(self):
        return self.name
    
class ProductAttributeValue(models.Model):
    """
    Valeurs des attributs
    """
    
    class Meta:
        verbose_name = "Valeur attribut"
        ordering = ['position']
        
    value              = models.CharField(max_length=100)
    product_attribute  = models.ForeignKey('ProductAttribute', verbose_name="Unité")
    position           = models.PositiveSmallIntegerField("Position", null=True, blank=True)
     
    def __unicode__(self):
        return u"{0} [{1}]".format(self.value, self.product_attribute)

Indiquons à Django que nous souhaitons voir ces modèles dans l'adminsitration:

backoffice/admin.py

from django.contrib import admin
from models import *

admin.site.register(Product)
admin.site.register(ProductItem)
admin.site.register(ProductAttribute)
admin.site.register(ProductAttributeValue)

Créons nos éléments:

test.py

#!/usr/bin/env python
#-*- coding: utf-8 -*-
import os

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eboutique.settings")

from django.db import models
from backoffice.models import *

# Création d'un attribut
product_attribute = ProductAttribute(name="couleur")
product_attribute.save()

# Création des valeurs des attributs
attribute1 = ProductAttributeValue(value="bleu", product_attribute=product_attribute, position=0)
attribute1.save()

attribute2 = ProductAttributeValue(value="jaune", product_attribute=product_attribute, position=0)
attribute2.save()

attribute2 = ProductAttributeValue(value="brun", product_attribute=product_attribute, position=0)
attribute2.save()

# Création du produit
product = Product(name="Tshirt", code="54065", price_ht=25, price_ttc=30)
product.save()

# Création d'une déclinaison de produit
product_item = ProductItem(product=product, code="5046", code_ean13="a1")
product_item.save()
product_item.attributes.add(attribute1)
product_item.attributes.add(attribute1)
product_item.save()

Rendez-vous sur la fiche de la déclinaison de produit:

On remarque qu'il est possible de sélectionner plusieurs valeurs dans un même champ (le champ attributes): c'est cela un manytomany

Many to many queryset

Dans notre fichier test.py (voir plus haut) nous avons vu comment ajouter en python une valeur dans cette relation many-to-many:

product_item.attributes.add(attribute1)

Il faudra cependant vous assurez que l'item product_item a bien été sauvegardé en base auparavant:

product_item.save()
product_item.attributes.add(attribute1)

Pour voir tous les attributs associés:

product_item.attributes.all()

D'autres exemples d'utilisation:

i1 = ProductItem.objects.get(pk=1)
# Tshirt [5046]
a1 = ProductAttributeValue.objects.get(pk=1)
# bleu [couleur]

ProductAttributeValue.objects.all()
# [<ProductAttributeValue: bleu [couleur]>, <ProductAttributeValue: jaune [couleur]>, <ProductAttributeValue: brun [couleur]>]

a1.product_item.all()
# [<ProductItem: Tshirt [5046]>]

ProductAttributeValue.objects.filter(product_item=i1)
# [<ProductAttributeValue: bleu [couleur]>, <ProductAttributeValue: brun [couleur]>]

ProductAttributeValue.objects.filter(product_item__id=1)
# [<ProductAttributeValue: bleu [couleur]>, <ProductAttributeValue: brun [couleur]>]

On remarque qu'il est possible de créer des queryset quelque soit l'item de départ (ProductAttributeValue ou ProductItem). Le reverse m2m nécessite cependant une information: related_name que nous avons renseigné dans l'exemple plus haut par la valeur product_item

Supprimer une relation many-to-many

Vous pouvez supprimer un item avec la méthode remove:

i1.attributes.remove(a1)

Ou dans l'autre sens:

a1.product_item.remove(i1)

Si vous voulez vider tous les attributs:

a1.product_item.clear()

Cette syntaxe fonctionne aussi:

a1.product_item = []