Bootcamp backendontwikkeling met FastAPI · Les

N+1-queries oplossen met DataLoaders

Batch en cache database-lookups met dataloaders om N+1-query-explosies in resolvers te voorkomen.

Les 2 van 413 stappen

N+1-queries oplossen met DataLoaders is een gratis Bootcamp backendontwikkeling met FastAPI-les op CoddyKit. Dit is les 2 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met FastAPI. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.

Het N+1-probleem in GraphQL

Met GraphQL kunnen clients geneste gegevens opvragen in één aanvraag, zoals een lijst met posts en de author van elk bericht. Het gevaar zit verborgen in de resolvers.

Stel dat je 100 berichten ophaalt met 1 query en daarna de auteur van elk bericht oplost met één query per bericht. Dat zijn 1 + 100 = 101 queries — het klassieke N+1-probleem.

  • 1 query om de lijst te laden (de 1)
  • N queries, één per item, om een gerelateerd veld te laden (de N)

Op schaal vernietigt dit de latentie en belast het de database zwaar. DataLoaders zijn de standaardoplossing.

N+1 zien in een Strawberry-resolver

Hier is een naïeve Strawberry-resolver die N+1 veroorzaakt. Elke author-resolver voert zijn eigen databaseaanroep uit.

Als een query 50 berichten retourneert, wordt deze author-resolver 50 afzonderlijke SELECT-instructies uitgevoerd. De lijstquery plus die 50 opzoekingen vormen de N+1-explosie.

import strawberry

@strawberry.type
class Author:
    id: int
    name: str

@strawberry.type
class Post:
    id: int
    title: str
    author_id: int

    @strawberry.field
    async def author(self) -> Author:
        # BAD: one DB round-trip per post -> N+1
        row = await db.fetch_one(
            "SELECT id, name FROM authors WHERE id = :id",
            {"id": self.author_id},
        )
        return Author(id=row["id"], name=row["name"])

Het kernidee: bundelen en cachen

Een DataLoader lost N+1 op met twee technieken:

  • Bundelen: in plaats van elke author_id direct op te lossen, verzamelt de loader alle sleutels die tijdens één stap van de eventlus worden aangevraagd en lost hij ze samen op in één gebundelde query (bijvoorbeeld WHERE id = ANY(...)).
  • Cachen: binnen één aanvraag wordt dezelfde sleutel maar één keer opgehaald. Tien keer vragen naar auteur 7 levert één opzoeking op.

Het resultaat: 1 query voor de berichten + 1 gebundelde query voor alle auteurs = 2 queries in plaats van 101.

Hoe bundelen werkt op de eventlus

Strawberry's DataLoader gebruikt de asyncio-eventlus. Wanneer meerdere resolvers loader.load(key) aanroepen, wordt de loader niet onmiddellijk uitgevoerd. Hij registreert elke sleutel en retourneert een wachtend awaitable-object.

Tijdens de volgende stap neemt de loader elke sleutel uit de wachtrij, roept hij je bundelfunctie één keer aan met de volledige lijst sleutels en lost hij vervolgens elk afzonderlijk awaitable-object op met het bijbehorende resultaat.

Daarom werken DataLoaders alleen in asynchrone code: het uitstelmechanisme is afhankelijk van het plannen van de gebundelde verwerking door de eventlus nadat het huidige synchrone werk is voltooid.

De functie voor het laden van bundels schrijven

Het hart van een DataLoader is de bundelfunctie. Deze ontvangt een lijst sleutels en moet een lijst resultaten retourneren in exact dezelfde volgorde als de sleutels.

Twee niet-onderhandelbare regels:

  • De lengte van de geretourneerde lijst moet gelijk zijn aan de lengte van de sleutellijst.
  • Het resultaat op index i moet bij keys[i] horen. Ontbrekende rijen moeten naar None (of een Exception) worden vertaald en mogen nooit worden weggelaten.

Hieronder brengen we rijen op basis van id in kaart en geven we ze vervolgens opnieuw uit in de volgorde van de sleutels.

from typing import List, Optional

async def load_authors(keys: List[int]) -> List[Optional[Author]]:
    rows = await db.fetch_all(
        "SELECT id, name FROM authors WHERE id = ANY(:ids)",
        {"ids": keys},
    )
    by_id = {row["id"]: Author(id=row["id"], name=row["name"]) for row in rows}
    # Preserve order; None for missing keys
    return [by_id.get(key) for key in keys]

Afstemming van de volgorde gedemonstreerd

Het contract voor het behouden van de volgorde is de meest voorkomende oorzaak van DataLoader-fouten. Hier is een zelfstandige simulatie: rijen komen in een willekeurige volgorde uit de database, maar we moeten ze uitlijnen met de opgevraagde sleutels.

Voer dit uit om te zien hoe een opzoekwoordenboek plus een comprehensie die op sleutelvolgorde is gebaseerd, een correcte uitlijning garandeert, zelfs wanneer de database rijen in een andere volgorde teruggeeft of een ontbrekende sleutel weglaat.

def batch_load(keys, rows):
    by_id = {row["id"]: row["name"] for row in rows}
    return [by_id.get(k) for k in keys]

keys = [3, 1, 7, 4]
# DB returns rows shuffled and is missing id=7
rows = [
    {"id": 1, "name": "Ada"},
    {"id": 4, "name": "Linus"},
    {"id": 3, "name": "Grace"},
]

result = batch_load(keys, rows)
print(result)  # ['Grace', 'Ada', None, 'Linus']
assert len(result) == len(keys)
for key, name in zip(keys, result):
    print(f"key={key} -> {name}")

Een DataLoader maken in Strawberry

Strawberry levert een DataLoader-klasse. Je maakt deze aan met je batchfunctie. Een aanroep van .load(key) geeft een awaitable terug dat na het batchgewijs verwerken wordt opgelost.

Belangrijk: een DataLoader-instantie bevat een cache per instantie. Je moet voor elk verzoek een nieuwe loader maken, zodat verouderde gegevens en lekken tussen gebruikers nooit kunnen optreden. Vervolgens koppelen we dit via de context.

from strawberry.dataloader import DataLoader

# batch function from the previous scene
author_loader = DataLoader(load_fn=load_authors)

# Inside a resolver you would now write:
#   author = await author_loader.load(self.author_id)
# Many concurrent .load() calls collapse into ONE call to load_authors.

Loaders per verzoek via GraphQL-context

De juiste plek om loaders met een verzoekbereik op te slaan is de GraphQL-context. Met FastAPI + Strawberry overschrijf je get_context om bij elk verzoek nieuwe loaders te maken.

Zo blijven het batchvenster en de cache beperkt tot één verzoek — precies de levensduur die je nodig hebt.

from strawberry.fastapi import GraphQLRouter
from strawberry.dataloader import DataLoader

async def get_context() -> dict:
    return {
        "author_loader": DataLoader(load_fn=load_authors),
        # one loader per relation, all rebuilt per request
    }

graphql_app = GraphQLRouter(schema, context_getter=get_context)
# app.include_router(graphql_app, prefix="/graphql")

De loader in een resolver gebruiken

De author-resolver leest nu de loader uit info.context en roept .load() aan. Strawberry voegt info automatisch toe wanneer je dit als parameter declareert.

Hoewel deze resolver één keer per bericht wordt uitgevoerd, worden al die aanroepen van .load() gebundeld in één SELECT ... WHERE id = ANY(...) — N+1 is opgelost.

import strawberry
from strawberry.types import Info

@strawberry.type
class Post:
    id: int
    title: str
    author_id: int

    @strawberry.field
    async def author(self, info: Info) -> Author:
        loader = info.context["author_loader"]
        return await loader.load(self.author_id)

Voordelen en beperkingen van caching

Binnen één verzoek cachet de loader op sleutel, zodat herhaalde aanroepen van load(7) de database maar één keer raadplegen. Dit is ideaal voor query's met veel vertakkingen waarin dezelfde auteur in meerdere berichten voorkomt.

Let op de afwegingen:

  • De cache is bewust per verzoek — deel een loader nooit tussen verzoeken, anders lever je verouderde gegevens.
  • Als een record midden in een verzoek verandert en je het opnieuw leest, krijg je de versie uit de cache. Roep na een mutatie loader.clear(key) aan om de cache ongeldig te maken.
  • De cachesleutel is de onbewerkte sleutelwaarde. Zorg dus dat sleutels hashbaar en consistent zijn (bijvoorbeeld altijd int, en niet soms str).

Collecties en tuplesleutels laden

DataLoaders zijn niet alleen bedoeld voor één-op-één-opzoekingen. Bij één-op-veel (de comments van een bericht) geeft de batchfunctie één lijst per sleutel terug. Groepeer de rijen op externe sleutel en geef daarna één lijst per opgevraagde sleutel terug (een lege lijst als er niets is).

Gebruik voor samengestelde opzoekingen een hashbare tuple als sleutel, bijvoorbeeld (post_id, locale). Houd het type stabiel, zodat caching correct blijft werken.

from collections import defaultdict

async def load_comments(post_ids):
    rows = await db.fetch_all(
        "SELECT id, post_id, body FROM comments WHERE post_id = ANY(:ids)",
        {"ids": post_ids},
    )
    grouped = defaultdict(list)
    for row in rows:
        grouped[row["post_id"]].append(row)
    # one list per key, in key order
    return [grouped.get(pid, []) for pid in post_ids]

Korte controle: levensduur van een DataLoader

Een teamgenoot maakt één DataLoader op moduleniveau en hergebruikt die voor de hele toepassing om "geheugen te besparen". Waarom is dit de verkeerde keuze voor een FastAPI GraphQL-service met meerdere gebruikers?

Samenvatting: DataLoaders lossen N+1 op

Je hebt geleerd hoe je explosies van N+1-query's in Strawberry + FastAPI-resolvers voorkomt:

  • N+1 ontstaat wanneer een geneste resolver één query per bovenliggend item uitvoert.
  • Een DataLoader lost dit op door alle sleutels uit één gebeurtenisloop-tik in één query te batchen en herhaalde sleutels binnen het verzoek te cachen.
  • De batchfunctie moet resultaten teruggeven die met de invoersleutels zijn uitgelijnd: dezelfde lengte en volgorde, met None of lege lijsten voor ontbrekende resultaten.
  • Maak loaders per verzoek in get_context en lees ze in resolvers uit info.context.
  • Gebruik lijsten per sleutel voor één-op-veelrelaties en hashbare tuplesleutels voor samengestelde opzoekingen; roep na mutaties clear() aan.

Met dit patroon blijven diep geneste GraphQL-query's snel en blijft je database rustig.

Gratis beginnen

Leer Bootcamp backendontwikkeling met FastAPI met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
21
Lessen
84

Veelgestelde vragen

Is de les “N+1-queries oplossen met DataLoaders” gratis?

Ja — je kunt hier op het web alle 3 lessen van het leerpad Bootcamp backendontwikkeling met FastAPI, waaronder “N+1-queries oplossen met DataLoaders”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.

Wat leer ik in “N+1-queries oplossen met DataLoaders”?

Batch en cache database-lookups met dataloaders om N+1-query-explosies in resolvers te voorkomen. Je oefent met Bootcamp backendontwikkeling met FastAPI door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Bootcamp backendontwikkeling met FastAPI te beginnen?

Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met FastAPI op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 2 van 4.

Hoe lang duurt de les “N+1-queries oplossen met DataLoaders”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met FastAPI?

Ja. Elke les over Bootcamp backendontwikkeling met FastAPI bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Typen, queries en mutations definiëren
  2. N+1-queries oplossen met DataLoaders
  3. Realtime GraphQL-subscriptions
  4. Querykostenanalyse en dieptelimieten
← Terug naar Bootcamp backendontwikkeling met FastAPI