Liez des utilisateurs avec des champs plusieurs-à-plusieurs
Jusqu’à présent, nous avons utilisé des relationsForeignKey
pour lier différents types de modèles les uns aux autres, ce qui crée des relations un-à-plusieurs (ou one-to-many). Par exemple, le modèlePhoto
a une relationForeignKey
àUser
par le champuploader
. Vous ne pouvez lier chaque instance dePhoto
qu’à une instance deUser
, mais vous pouvez lier une instance deUser
à de nombreuses instances différentes de Photo
.
Vous pouvez également utiliser le OneToOneField
pour créer des relations un-à-un (one-to-one). Dans ce cas, vous ne pouvez lier chaque instance du modèle A qu’à une instance du modèle B, et vice-versa.
Un cas d’usage courant pour les relations un-à-un : l’utilisation des modèles de profil pour étendre le modèle User
. Dans notre cas, comme nous avons deux types d’utilisateurs — les créateurs et les abonnés — nous voudrions peut-être créer un modèle Creator
et un modèle Subscriber
, chacun ayant un OneToOneField
relatif au User
. Ceux-ci contiendront les informations nécessaires pour chaque utilisateur, mais qui ne sont significatives que pour les types créateur et abonné.
Enfin, vous avez également l’option du ManyToManyField
, que vous pouvez utiliser pour créer des relations plusieurs-à-plusieurs. Imaginons que le modèle A a un ManyToManyField
qui le lie au modèle B. Cela signifie qu’il peut être lié à plusieurs instances différentes du modèle B, et aussi qu’une instance du modèle B peut être liée à plusieurs instances différentes du modèle A.
Sur notre site, nous allons utiliser des relations plusieurs-à-plusieurs pour permettre aux utilisateurs de suivre d’autres utilisateurs. Nous voulons qu’un utilisateur puisse s’abonner à de nombreux créateurs différents, et que de nombreux autres utilisateurs puissent aussi s’abonner à lui. La relation plusieurs-à-plusieurs est donc parfaitement adaptée.
Relation plusieurs-à-plusieurs
Implémentons maintenant ce schéma dans nos modèles. Découvrez en vidéo comment créer une relation plusieurs à plusieurs-à-plusieurs avec Django :
Étape 1 : Mettez à jour les modèles
Pour établir une relation plusieurs-à-plusieurs entre les utilisateurs, vous devez spécifier un ManyToManyField
sur le modèle User
, qui lie à un autre User
. Appelons le nôtre follows (suit)
.
# authentication/models.py
class User(AbstractUser):
...
follows = models.ManyToManyField(
'self',
limit_choices_to={'role': CREATOR},
symmetrical=False,
verbose_name='suit',
)
Le premier argument dans le ManyToManyField
est le modèle avec lequel vous nouez une relation. Dans notre cas, il s’agit du même modèleUser
, auquel nous référons avec'self'
.
Vous pouvez limiter quels utilisateurs peuvent être suivis en utilisant le mot-clé optionnel limit_choices_to
. Nous voulons que seuls les utilisateurs avec le rôle CREATOR
puissent être suivis.
Dans ce cas particulier, où les deux modèles dans la relation plusieurs-à-plusieurs sont les mêmes, vous devez également préciser si la relation est symétrique. Les relations symétriques sont celles où il n’y a aucune différence entre les deux acteurs de la relation, comme quand on lie deux amis. Un utilisateur en suit un autre, donc vous précisez symmetrical=False
. L’argument symmetrical
n’est pas requis si vous liez à un autre modèle que celui dans lequel le ManyToManyField
est déclaré.
Maintenant, générez et exécutez les migrations :
(ENV) ~/fotoblog (master)
→ python manage.py makemigrations
Migrations for 'authentication':
authentication/migrations/0003_user_follows.py
- Add field follows to user
(ENV) ~/fotoblog (master)
→ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, authentication, blog, contenttypes, sessions
Running migrations:
Applying authentication.0004_user_follows... OK
Ensuite, voyons comment utiliser des formulaires pour créer des relations plusieurs-à-plusieurs au sein d’une vue.
Étape 2 : Créez des relations plusieurs-à-plusieurs dans un formulaire
Pour créer la relation plusieurs-à-plusieurs dans la vue, vous devez d’abord définir le formulaire approprié. Comme les champs sont dans le modèleUser
, vous pouvez utiliser unmodelForm
.
Créez un formulaire qui permette à l’utilisateur de choisir d’autres utilisateurs qu’il veut suivre.
# blog/forms.py
from django.contrib.auth import get_user_model
User = get_user_model()
class FollowUsersForm(forms.ModelForm):
class Meta:
model = User
fields = ['follows']
Étape 3 : Créez la vue
Gérez maintenant le formulaire dans une vue :
# blog/views.py
@login_required
def follow_users(request):
form = forms.FollowUsersForm(instance=request.user)
if request.method == 'POST':
form = forms.FollowUsersForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
return redirect('home')
return render(request, 'blog/follow_users_form.html', context={'form': form})
Avec le ManyToManyField
, vous pouvez utiliser la fonction ModelForm.save()
pour créer la relation plusieurs-à-plusieurs.
Étape 4 : Créez le gabarit
Ajoutez ensuite le gabarit, comme d’habitude.
# blog/templates/blog/follow_users_form.html
{% extends 'base.html' %}
{% block content %}
Suivre des utilisateurs
method="post"
{{ form.as_p }}
{% csrf_token %}
type="submit" Confirmer
{% endblock content %}
Étape 5 : Ajoutez le modèle d’URL
Ajoutez maintenant le modèle d’URL pour la nouvelle page.
# fotoblog/urls.py
urlpatterns = [
…
path('follow-users/', blog.views.follow_users, name='follow_users')
]
Étape 6 : Ajoutez un lien vers la nouvelle page
Ajoutez un lien dans la barre latérale.
# templates/base.html
…
{% if perms.blog.add_photo %}
href="{% url 'photo_upload' %}"Télécharger une photo
href="{% url 'create_multiple_photos' %}"Télécharger plusieurs photos
{% endif %}
href="{% url 'follow_users' %}"Suivre des utilisateurs
href="{% url 'upload_profile_photo' %}"Changer la photo de profil
...
Voyons à quoi ressemble ce formulaire :
Nous avons réussi ! Le formulaire contient tous les créateurs, et permet à l’utilisateur de les sélectionner et de les suivre. Il vous permet aussi de sélectionner plusieurs utilisateurs en maintenant le bouton CTRL/Commande appuyé. Le formulaire les stocke alors dans un champ plusieurs-à-plusieurs.
Voyons ensuite comment stocker des informations sur les relations plusieurs-à-plusieurs.
Stockez des données supplémentaires sur les relations plusieurs-à-plusieurs avec des tables intermédiaires
Vous aurez parfois besoin de stocker des données supplémentaires sur la relation entre deux instances du modèle liées par une relation plusieurs-à-plusieurs.
Disons que vous voulez autoriser un modèle Blog
à avoir plusieurs contributeurs, et stocker un log de ce que chaque auteur a ajouté.
Pour cela, créez une table intermédiaire afin de stocker des données sur la relation. Lorsque vous utilisez un ManyToManyField
, Django produit lui-même cette table. Ici, vous la créez simplement manuellement à la place. Découvrez la procédure en vidéo :
Étape 1 : Créez la table intermédiaire
La table intermédiaire nécessite deux relations ForeignKey
aux deux modèles impliqués dans la relation plusieurs-à-plusieurs. Pour notre application, c’est donc une relation vers le User
, et une vers le Blog
.
Nous allons également ajouter le champ contribution
pour stocker des informations sur les contributions spécifiques de chaque auteur.
Voyons de quoi ça a l’air :
# blog/models.py
class BlogContributor(models.Model):
contributor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
contribution = models.CharField(max_length=255, blank=True)
class Meta:
unique_together = ('contributor', 'blog')
Nous avons défini l’attribut unique_together
dans la classe Meta
pour garantir qu’il n’y a qu’une seule instance de BlogContributor
pour chaque paire contributor
- blog
.
Étape 2 : Mettez à jour le ManyToManyField
pour utiliser la table intermédiaire
Ajoutez maintenant un ManyToManyField
à Blog
et dites-lui d’utiliser notre table intermédiaire.
Pour accomplir cela, spécifiez l’argument mot-clé through
en déclarant le champ. Vous devez simplement lui montrer la direction du nouveau modèle. Pour l’instant, laissons l’ancien champ author
en place, mais nous allons le rendre optionnel en l’autorisant à être null
.
class Blog(models.Model):
...
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
contributors = models.ManyToManyField(settings.AUTH_USER_MODEL, through='BlogContributor')
...
Générez maintenant les migrations.
→ python manage.py makemigrations
SystemCheckError: System check identified some issues:
ERRORS:
blog.Blog.author: (fields.E304) Reverse accessor for 'blog.Blog.author' clashes with reverse accessor for 'blog.Blog.contributors'.
HINT: Add or change a related_name argument to the definition for 'blog.Blog.author' or 'blog.Blog.contributors'.
blog.Blog.contributors: (fields.E304) Reverse accessor for 'blog.Blog.contributors' clashes with reverse accessor for 'blog.Blog.author'.
HINT: Add or change a related_name argument to the definition for 'blog.Blog.contributors' or 'blog.Blog.author'.
Oh non ! Django n’a pas l’air d’aimer nos changements ! Vous voyez ceci en raison d’un conflit dans notre accesseur inversé.
Avant d’ajouter ce champ, vous auriez pu utiliser user.blog_set
pour accéder à un QuerySet des instances deBlog
de cet auteur précis.
Mais maintenant, si nous voulons faire la même chose pour le champ contributors
— en récupérant toutes les instances de Blog
pour unUser
dans lesquelles l’utilisateur est présent dans les contributors
de l’instance deBlog
— à nouveau, la recherche inversée serait user.blog_set
.
Pour contourner ceci, donnez au champ un accesseur inversé personnalisé en spécifiant l’argument related_name
. Si vous le définissez comme contributions
, vous pouvez alors accéder à toutes les instances de Blog
ayant le User
comme contributeur, en utilisant user.contributions
à la place.
Mettez à jour le modèle en tenant compte de tout cela.
# blog/models.py
class Blog(models.Model):
...
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
contributors = models.ManyToManyField(
settings.AUTH_USER_MODEL, through='BlogContributor', related_name='contributions')
...
Et réessayez de générer les migrations.
(ENV) ~/fotoblog (master)
→ python manage.py makemigrations
Migrations for 'blog':
blog/migrations/0003_auto_20210427_2325.py
- Create model BlogContributor
- Add field contributors to blog
Cela a fonctionné ! Vous pouvez les appliquer :
(ENV) ~/fotoblog (master)
→ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, authentication, blog, contenttypes, sessions
Running migrations:
Applying blog.0003_auto_20210427_2325... OK
Étape 3 : Mettez à jour la vue pour gérer une relation plusieurs-à-plusieurs
Ensuite, mettez à jour la vue blog_and_photo_upload
pour refléter ces changements.
Voyons comment la vue gère actuellement une requête POST
:
def blog_and_photo_upload(request):
...
if request.method == 'POST':
blog_form = forms.BlogForm(request.POST)
photo_form = forms.PhotoForm(request.POST, request.FILES)
if all([blog_form.is_valid(), photo_form.is_valid()]):
photo = photo_form.save(commit=False)
photo.uploader = request.user
photo.save()
blog = blog_form.save(commit=False)
blog.photo = photo
blog.author = request.user
blog.save()
...
Vous pouvez uniquement stocker les relations dans le ManyToManyField
une fois que le modèle a été sauvegardé dans la base de données.
Dans notre cas, nous voulons ajouter l’utilisateur connecté en tant que contributeur, une fois que l’instance de blog
est sauvegardée.
Pour faire cela, utilisez la méthode add
pour créer la relation plusieurs-à-plusieurs. Vous pouvez aussi préciser le contenu des champs supplémentaires avec l’argument nommé through_defaults
.
Mettez à jour la vue pour utiliser le nouveau ManyToManyField
:
def blog_and_photo_upload(request):
...
if request.method == 'POST':
blog_form = forms.BlogForm(request.POST)
photo_form = forms.PhotoForm(request.POST, request.FILES)
if all([blog_form.is_valid(), photo_form.is_valid()]):
photo = photo_form.save(commit=False)
photo.uploader = request.user
photo.save()
blog = blog_form.save(commit=False)
blog.photo = photo
blog.save()
blog.contributors.add(request.user, through_defaults={'contribution': 'Auteur principal'})
return redirect('home')
...
La relation de contributeur a maintenant été sauvegardée comme une relation plusieurs-à-plusieurs.
Nous avons ajouté la capacité d’ajouter des contributeurs multiples pour un billet de blog, mais nous devons encore migrer les anciennes données stockées dans le champ author
vers le nouveau modèle de données. C’est ce que vous allez faire maintenant !
C'est à vous ! Migrez d’une ForeignKey
à un ManyToManyField
en utilisant une migration personnalisée
Maintenant que notre nouvelle relation est configurée, vous pouvez migrer les données de l’ancienne relation un-à-plusieurs vers une relation plusieurs-à-plusieurs.
Voici ce que vous devez faire :
Créez une migration vide dans l’application
blog
.Dans la migration, itérez dans chaque instance du modèle
Blog
. Ajoutez la valeurauthor
auxcontributors
, avec une contribution par défaut de'Auteur principal'
pour chacun d’entre eux.Exécutez la migration. Si le résultat vous convient, supprimez le champ
author
deBlog
.Créez et exécutez les nouvelles migrations. Vous devriez avoir migré avec succès toutes les données existantes vers le nouveau modèle de données !
Une fois que vous avez écrit votre migration personnalisée, comparez-la à la solution dans le dépôt GitHub.
En résumé
Utilisez le
ManyToManyField
pour stocker des relations plusieurs-à-plusieurs.Les relations plusieurs-à-plusieurs sont stockées dans des tables intermédiaires.
Pour stocker des données sur une relation, créez une table intermédiaire enrichie de champs additionnels et spécifiez-la avec le mot-clé
through
.
Vous connaissez maintenant les relations plusieurs-à-plusieurs ! Vérifions vos compétences sur les modèles et les formulaires, avant de passer à l’ORM Django et aux gabarits.