Comprendre correctement ", ", ", et ".

Un tour d’horizon complet de user_id, user.id et user__id

Vous avez probablement déjà rencontré ces expressions dans votre code Django.

Post.objects.filter(user_id=1)
Post.objects.filter(user__id=1)
post.user.id
post.user_id

Avec l’underscore _, le double underscore __ et le point ., il est facile de se perdre. Mélanger ces symboles sans distinction peut entraîner des différences subtiles de comportement et de performance.

Dans cet article, nous allons aborder :

  • Ce que signifient exactement ", ", ", et ".
  • Les différences de signification et de performance entre user_id, user.id et user__id.
  • Quand utiliser chaque forme dans la pratique.

1. Vue d’ensemble : rôle de chaque symbole



", " (un underscore)

  • C’est simplement une partie du nom.
  • Exemples : created_at, user_id.
  • En particulier, Django crée automatiquement une colonne <nom_du_champ>_id dans la base de données pour les ForeignKey.

", " (double underscore)

  • C’est le séparateur de lookup propre à Django ORM.
  • Il n’a de sens que dans les noms d’arguments de filter(), exclude(), order_by(), values(), etc.
  • Exemples :
  • age__gte=20 (champ + condition)
  • user__email='a@b.com' (accès à un champ d’un modèle lié)
  • created_at__date=... (transformation + condition)

"." (point)

  • C’est l’opérateur d’accès aux attributs en Python.
  • obj.user, obj.user.id, qs.filter(...).order_by(...) utilisent tous le point.
  • Du point de vue de l’ORM, c’est simplement l’accès à un attribut d’un objet Python déjà en mémoire (une requête supplémentaire peut être déclenchée si l’objet n’est pas encore chargé – lazy loading).

2. Signification de ", " et ", " autour des ForeignKey

2.1 Exemple de définition de modèle

from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()

class Post(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)

Avec cette définition, la base de données contient :

  • Table post
  • id (PK)
  • user_id (colonne FK réelle)
  • title

Django crée :

  • Un attribut post.user (instance User).
  • Un attribut post.user_id (int ou type PK).
post.user    # -> instance User (requête supplémentaire si nécessaire)
post.user_id # -> PK de User (déjà disponible, pas de requête supplémentaire)

2.2 Utilisation de _id dans les requêtes

# 1) Passer l’objet User
Post.objects.filter(user=request.user)

# 2) Passer la valeur PK
Post.objects.filter(user=request.user.id)

# 3) Utiliser directement le nom de colonne
Post.objects.filter(user_id=request.user.id)

Ces trois expressions se traduisent en la même clause WHERE (comparaison de PK). Le suffixe _id est simplement le nom de la colonne réelle dans la base de données.


3. Chaîne de double underscore : le séparateur de lookup de Django ORM



Django appelle __ le séparateur de lookup. Il permet de concaténer des champs, de traverser des relations ou d’ajouter des transformations.

3.1 Modèle de base

  1. champ__lookup * Ex : age__gte=20, name__icontains='kim'
  2. relation__autre_champ * Ex : user__email='a@b.com', profile__company__name='...'
  3. champ__transform__lookup * Ex : created_at__date=date.today(), created_at__year__gte=2024

Exemple :

# 1) Comparaison simple
Post.objects.filter(title__icontains="django")

# 2) Traverser une relation
Post.objects.filter(user__email__endswith="@example.com")

# 3) Comparer l’année d’un champ date
Post.objects.filter(created_at__year=2025)

Lorsque vous traversez une relation (user__email), une jointure SQL est générée.


4. Le point "." et les attributs user.id / user_id

4.1 Différence dans les instances Django

post = Post.objects.first()

post.user       # instance User (requête supplémentaire si non chargée)
post.user.id    # PK de l’instance User
post.user_id    # valeur FK déjà présente dans `post`

Points clés :

  • Accéder à post.user pour la première fois déclenche une requête supplémentaire (lazy loading).
  • post.user_id est déjà disponible, aucune requête supplémentaire.
  • Si vous n’avez besoin que de la PK, utilisez user_id pour éviter des requêtes inutiles.

4.2 Exemple de performance : page de liste

# views.py
posts = Post.objects.all()   # sans select_related

for post in posts:
    print(post.user.id)
  • La requête initiale : 1
  • À chaque itération, post.user déclenche une requête supplémentaire → N+1 problème.

En revanche :

for post in posts:
    print(post.user_id)
  • Pas de requête supplémentaire.

Si vous utilisez select_related("user"), post.user ne déclenchera pas de requête supplémentaire.


5. Comparaison précise de user_id, user.id et user__id

5.1 user_id – utilisation directe de la colonne DB

  • Où ? : instance (post.user_id) ou requête (filter(user_id=1)).
  • Signification : colonne FK réelle.
  • Performance : aucune requête supplémentaire.

5.2 user.id – accès à la PK via l’objet lié

  • Où ? : uniquement dans le code Python (post.user.id).
  • Signification : charger l’objet User puis lire son id.
  • Performance : requête supplémentaire si l’objet n’est pas déjà chargé.

5.3 user__id – condition de lookup dans la requête

  • Où ? : uniquement dans les arguments de requête (filter(user__id=1)).
  • Signification : traverser la relation user et appliquer une condition sur id.
  • Performance : génère une jointure SQL, mais l’ORM peut optimiser vers user_id = 1 dans certains cas.

6. Résumé des patterns fréquents

# 1) Utiliser la colonne FK directement
Post.objects.filter(user_id=1)

# 2) Passer l’objet User ou sa PK
Post.objects.filter(user=1)

# 3) Traverser la relation pour filtrer
Post.objects.filter(user__id=1)
Post.objects.filter(user__email__icontains="@example.com")

En pratique :

  • Filtrer par PK : user_id=... ou user=....
  • Filtrer par un champ d’un modèle lié : user__email=..., user__is_active=True.

7. Règles pratiques à retenir

  1. Filtrer par PK : Post.objects.filter(user=request.user) ou user_id=request.user.id – les deux sont valides.
  2. Filtrer par un champ lié : utilisez la chaîne __ : user__is_active=True.
  3. Dans les templates ou vues : si vous n’avez besoin que de la PK, utilisez {{ post.user_id }} pour éviter N+1.
  4. Si vous utilisez plusieurs champs de User : faites select_related("user") puis accédez à post.user.<champ>.
  5. Gardez à l’esprit que chaque __ peut introduire une jointure : surveillez les requêtes générées.

8. En un clin d’œil

  • user_id : colonne FK réelle.
  • user.id : PK de l’objet User (requête supplémentaire si non chargé).
  • user__id : condition de lookup traversant la relation.
  • __ : séparateur de lookup de l’ORM.
  • . : accès aux attributs Python.

Comprendre ces distinctions vous aide à écrire des requêtes plus claires, à éviter les problèmes de performance et à rendre votre code plus lisible pour l’équipe.

image