Entendiendo correctamente _, __ y . en el ORM de Django

Resumen de user_id, user.id y user__id

Probablemente ya hayas visto estos elementos en tu código Django.

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

La mezcla de guiones bajos, dobles guiones bajos y puntos puede resultar confusa. Si empiezas a usarlos sin distinguirlos, aparecerán diferencias sutiles de comportamiento y rendimiento.

En este artículo cubriremos:

  • ¿Qué significa exactamente cada símbolo en el ORM de Django?
  • Diferencias de significado y rendimiento entre user_id, user.id y user__id
  • Recomendaciones prácticas sobre cuándo usar cada uno

1. Visión general: el papel de los tres símbolos



1.1 _ (un guion bajo)

  • Es simplemente parte del nombre.
  • Ejemplos: created_at, user_id.
  • En Django, una ForeignKey crea automáticamente una columna <campo>_id en la base de datos.

1.2 __ (doble guion bajo)

  • Es el separador de búsqueda exclusivo del ORM de Django.
  • Solo tiene significado dentro de los nombres de argumentos de filter(), exclude(), order_by(), values(), etc.
  • Ejemplos:
  • age__gte=20 (campo + condición)
  • user__email='a@b.com' (acceder a un campo de un modelo relacionado)
  • created_at__date=... (transformar y filtrar)

1.3 . (punto)

  • Es el operador de acceso a atributos de Python.
  • Ejemplos: obj.user, obj.user.id, qs.filter(...).order_by(...).
  • En el ORM, simplemente accede a un atributo de un objeto Python que ya está en memoria (puede disparar una consulta adicional si el objeto relacionado no está cargado).

2. _ y _id: significado alrededor de ForeignKey

2.1 Definición del modelo

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)

Esto crea la tabla post con columnas:

  • id (PK)
  • user_id (columna FK real)
  • title

Django crea dos atributos en el objeto Python:

  • post.user → instancia de User (puede disparar una consulta)
  • post.user_id → entero (valor PK, sin consulta adicional)

2.2 Usar _id en consultas

# 1) Pasar la instancia de User
Post.objects.filter(user=request.user)

# 2) Pasar el PK de User
Post.objects.filter(user=request.user.id)

# 3) Usar directamente el nombre de columna
Post.objects.filter(user_id=request.user.id)

Las tres formas se compilan en la misma condición WHERE "post"."user_id" = 1.


3. __ en la cadena de búsqueda: el separador de búsqueda del ORM



El separador __ permite encadenar relaciones, transformaciones y condiciones.

3.1 Patrones comunes

  1. campo__lookupage__gte=20
  2. relacion__campouser__email='a@b.com'
  3. campo__transform__lookupcreated_at__date=date.today()

Ejemplo:

# 1) Comparación simple
Post.objects.filter(title__icontains="django")

# 2) Condición a través de una relación
Post.objects.filter(user__email__endswith="@example.com")

# 3) Comparar solo el año de una fecha
Post.objects.filter(created_at__year=2025)

Cuando se atraviesa una relación, el ORM genera un JOIN.


4. . y los atributos user.id vs user_id

4.1 Diferencia en instancias de Django

post = Post.objects.first()

post.user       # instancia de User (puede disparar una consulta)
post.user.id    # PK de la instancia de User
post.user_id    # valor FK ya presente en `post`
  • post.user puede disparar una consulta adicional si el objeto no está cargado.
  • post.user_id no dispara ninguna consulta adicional.

4.2 Ejemplo de rendimiento en una lista

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

for post in posts:
    print(post.user.id)  # 1 consulta por post (N+1)

En cambio:

for post in posts:
    print(post.user_id)  # sin consultas adicionales

Si usas select_related("user"), post.user ya está cargado y no habrá consultas adicionales.


5. Comparación clara entre user_id, user.id y user__id

5.1 user_id – usar la columna FK directamente

  • En instancias: post.user_id.
  • En consultas: filter(user_id=1), order_by("user_id").
  • No dispara consultas adicionales.

5.2 user.id – acceder al PK a través del objeto relacionado

  • Solo en Python: post.user.id.
  • Puede disparar una consulta si post.user no está cargado.
  • Con select_related("user"), no hay consultas adicionales.

5.3 user__id – usar el separador de búsqueda en consultas

  • Solo en argumentos de consulta: Post.objects.filter(user__id=1).
  • Genera un JOIN y una condición WHERE user.id = 1.
  • En la práctica, el ORM puede optimizar a user_id = 1 cuando sea posible, pero conceptualmente es una consulta que atraviesa la relación.

6. Resumen de patrones confusos

# 1) Usar la columna FK directamente
Post.objects.filter(user_id=1)

# 2) Pasar la instancia o el PK
Post.objects.filter(user=1)

# 3) Usar la relación en la búsqueda
Post.objects.filter(user__id=1)
Post.objects.filter(user__email__icontains="@example.com")

Regla práctica:

  • Para filtrar por el PK de la FK: user_id=... o user=....
  • Para filtrar por otro campo del modelo relacionado: user__campo=....

7. Recomendaciones para la práctica

  1. Filtrar por PK de la FK python Post.objects.filter(user=request.user) Post.objects.filter(user_id=request.user.id) Ambos son aceptables; la elección depende de la legibilidad.

  2. Filtrar por otro campo del modelo relacionado python Post.objects.filter(user__is_active=True) Post.objects.filter(user__email__endswith="@example.com")

  3. En plantillas o vistas cuando solo necesitas el PK django {{ post.user_id }} {# sin consultas adicionales #} {{ post.user.id }} {# puede disparar una consulta #}

  4. Si usas select_related("user") python posts = Post.objects.select_related("user") for post in posts: print(post.user.username) Evita el problema N+1.

  5. Ten en cuenta los JOIN * Cada __ que atraviesa una relación puede generar un JOIN adicional. * En consultas complejas, revisa el plan de ejecución para evitar sobrecarga.


8. Conclusión

  • _ → parte del nombre, especialmente <campo>_id para FK.
  • __ → separador de búsqueda del ORM; encadena campos, relaciones y transformaciones.
  • . → acceso a atributos de Python; puede disparar consultas adicionales.
  • user_id → valor FK en la tabla post.
  • user.id → PK de la instancia User (puede requerir consulta).
  • user__id → condición en la consulta que atraviesa la relación user.

Conocer estas diferencias te permite escribir consultas más claras, evitar consultas N+1 y optimizar el rendimiento de tu aplicación Django.

image