Begrijpen van _, __ en . in Django ORM

Een overzicht van user_id, user.id en user__id in één keer

Je hebt deze constructies waarschijnlijk al gezien in Django-code.

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

Met de onderstrepingstekens _, dubbele onderstrepingstekens __ en het punt . die allemaal door elkaar heen lopen, is het makkelijk om te verdwalen. Als je ze zomaar door elkaar gebruikt, ontstaan subtiele gedragsverschillen én prestatieproblemen.

In dit artikel behandelen we:

  • Wat _, __ en . precies betekenen in Django ORM
  • De betekenis en prestatieverschillen tussen user_id, user.id en user__id
  • Praktische richtlijnen voor wanneer je welke syntaxis moet gebruiken

1. Het grote plaatje: de rol van de drie symbolen



Kort samengevat:

  • _ (één onderstrepingsteken)
  • Het is slechts een onderdeel van de naam.
  • Voorbeelden: created_at, user_id.
  • In Django maakt een ForeignKey automatisch een kolom <veldnaam>_id in de database.

  • __ (twee onderstrepingstekens)

  • De "lookup separator" van Django ORM.
  • Alleen binnen de argumentnamen van filter(), exclude(), order_by(), values() etc. heeft het betekenis.
  • Voorbeelden:

    • age__gte=20 (veld + voorwaarde)
    • user__email='a@b.com' (toegang tot een veld via een relatie)
    • created_at__date=... (transformatie + voorwaarde)
  • . (punt)

  • De attribuuttoegang in Python.
  • Voorbeelden: obj.user, obj.user.id, qs.filter(...).order_by(...).
  • Vanuit het ORM‑perspectief is dit simpelweg toegang tot een attribuut van een Python‑object. (Eventueel kan er een extra query worden uitgevoerd – Lazy Loading.)

Laten we nu dieper ingaan op elk van deze symbolen vanuit het Django‑ORM‑perspectief.


2. _ en _id: betekenis rond ForeignKey

2.1 Voorbeeld van een modeldefinitie

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)

Dit resulteert in de volgende kolommen in de database:

  • post tabel
  • id (PK)
  • user_id (de echte FK‑kolom)
  • title

Django creëert:

  • Een attribuut post.user (een User‑instantie)
  • Een attribuut post.user_id (de integer‑waarde van de PK)
post.user    # -> User object (kan een extra query triggeren)
post.user_id # -> PK‑waarde van User (al aanwezig, geen extra query)

2.2 Gebruik van _id in queries

Voor een ForeignKey kun je op verschillende manieren filteren:

# 1) Een User‑object als waarde
Post.objects.filter(user=request.user)

# 2) De PK‑waarde van de User
Post.objects.filter(user=request.user.id)

# 3) Direct de kolomnaam gebruiken
Post.objects.filter(user_id=request.user.id)

Deze drie benaderingen resulteren uiteindelijk in dezelfde WHERE‑clausule (voor een eenvoudige PK‑vergelijking).

Samenvatting: _id is de daadwerkelijke kolomnaam in de database en wordt in het ORM gebruikt wanneer je die kolom rechtstreeks wilt aanspreken.


3. __ dubbele onderstreping: de lookup separator van Django ORM



Dit is het unieke Django‑specifieke concept.

In de officiële documentatie wordt __ aangeduid als de "ORM lookup separator". Het wordt gebruikt om velden te koppelen, relaties te traverseren of voorwaarden/transformaties toe te voegen.

3.1 Basispatroon

Er zijn drie hoofdpatronen:

  1. veldnaam__lookup * Voorbeeld: age__gte=20, name__icontains='kim'
  2. relatieveld__ander_veld * Voorbeeld: user__email='a@b.com', profile__company__name='...'
  3. veld__transform__lookup * Voorbeeld: created_at__date=date.today(), created_at__year__gte=2024

Voorbeeld:

# 1) Eenvoudige vergelijking
Post.objects.filter(title__icontains="django")

# 2) Via een relatie
Post.objects.filter(user__email__endswith="@example.com")

# 3) Alleen het jaar van een datumveld
Post.objects.filter(created_at__year=2025)

Wanneer je een relatie traverses zoals user__email, wordt er een JOIN uitgevoerd in de database.

Kernpunt: elke keer dat __ voorkomt, doet het ORM iets complexer (JOIN, functie, voorwaarde).


4. . puntoperator en user.id, user_id

Laten we nu user.id en user_id naast elkaar bekijken.

4.1 Verschillen in een Django‑instantie

post = Post.objects.first()

post.user       # User object (kan een extra SELECT triggeren)
post.user.id    # PK van het User object
post.user_id    # De FK‑waarde die al in de Post‑instantie staat

Belangrijk:

  • Het eerste gebruik van post.user kan een extra query veroorzaken (Lazy Loading).
  • post.user_id is al aanwezig, dus geen extra query.
  • Als je alleen de PK nodig hebt, is user_id lichter.

4.2 Prestatievoorbeeld: lijstpagina

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

for post in posts:
    print(post.user.id)
  • De initiële query voor posts: 1 keer.
  • Bij elke iteratie die post.user aanspreekt, wordt er een extra query uitgevoerd. → Bij 100 posts: 100 extra queries (N+1 probleem).

In plaats daarvan:

for post in posts:
    print(post.user_id)
  • user_id is al geladen, dus geen extra query.

Of gebruik select_related:

posts = Post.objects.select_related("user")

for post in posts:
    print(post.user.id)  # geen extra query

5. user_id, user.id, user__id – een nauwkeurige vergelijking

5.1 user_id – directe DB‑kolom

  • Waar? Model‑instantie (post.user_id) en ORM‑query (filter(user_id=1), order_by("user_id")).
  • Betekenis: de echte FK‑kolom in de post‑tabel.
  • Prestatie: geen extra query bij lezen; in een query resulteert in een eenvoudige WHERE‑clausule.

5.2 user.id – via relatie

  • Waar? Alleen in Python‑objecten (post.user.id).
  • Betekenis: eerst het User‑object laden, dan de id‑attribuut lezen.
  • Prestatie: kan een extra query triggeren tenzij select_related("user") is gebruikt.

Samenvatting: gebruik user_id als je alleen de waarde nodig hebt; gebruik user.<attribuut> als je ook andere velden van User nodig hebt.

5.3 user__id – ORM‑lookup via relatie

  • Waar? Alleen in de argumentnamen van ORM‑methoden (filter(user__id=1)).
  • Betekenis: een JOIN naar de User‑tabel en een voorwaarde op id.
  • Prestatie: kan een JOIN veroorzaken; in eenvoudige gevallen kan het worden geoptimaliseerd tot user_id = 1.

6. _ vs __ – verwarrende patronen overzichtelijk maken

6.1 Veelvoorkomende combinaties in filters

# 1) Directe kolomnaam (_id)
Post.objects.filter(user_id=1)

# 2) FK‑veld met PK‑waarde
Post.objects.filter(user=1)

# 3) Relatie traversen met lookup (__)
Post.objects.filter(user__id=1)
Post.objects.filter(user__email__icontains="@example.com")

In de praktijk geldt:

  • Voor een eenvoudige PK‑filter: user_id=... of user=request.user.
  • Voor een ander veld van de gerelateerde model: user__email=..., user__is_active=True.

6.2 _ is "naamdeel", __ is "ORM‑operator"

  • _ (underscore) verschijnt overal: variabelen, model‑velden, DB‑kolommen.
  • __ (double underscore) is uitsluitend een syntactische constructie binnen ORM‑queries.

7. Praktische regels voor de praktijk

  1. Filteren op FK‑waarde python Post.objects.filter(user=request.user) Post.objects.filter(user_id=request.user.id) Beide zijn acceptabel; kies voor leesbaarheid.

  2. Filteren op een ander veld van de gerelateerde model python Post.objects.filter(user__is_active=True) Post.objects.filter(user__email__endswith="@example.com")

  3. In templates of views alleen de FK‑waarde nodig django {{ post.user_id }} {# lichtgewicht, geen extra query #} {{ post.user.id }} {# kan extra query triggeren #}

  4. Veelgebruikte User‑velden python posts = Post.objects.select_related("user") for post in posts: print(post.user.username, post.user.email)

  5. Wees bewust van JOIN‑overhead bij __ * Vooral bij omgekeerde FK‑relaties of meerdere filter()‑ketens.


8. Samenvatting in één regel

  • _ : gewoon een naamdeel; <veld>_id is de echte FK‑kolom.
  • __ : de ORM‑lookup separator; koppelt velden, relaties, transformaties en voorwaarden.
  • . : Python‑attribuuttoegang; user.id laadt eerst het User‑object.
  • user_id : de FK‑waarde in de post‑tabel.
  • user.id : de PK‑waarde van het User‑object.
  • user__id : een ORM‑lookup die via de relatie naar User.id filtert.

Door deze onderscheidingen te kennen, kun je:

  • Je query‑intentie verduidelijken
  • Onnodige JOINs en N+1‑queries vermijden
  • Code‑reviews soepeler laten verlopen

image