Kem Intensif Pembangunan Bahagian Belakang FastAPI · Pelajaran

Menyelesaikan Pertanyaan N+1 dengan DataLoaders

Kelompokkan dan cache carian pangkalan data menggunakan pemuat data untuk menghapuskan ledakan pertanyaan N+1 dalam penyelesai.

Pelajaran 2 daripada 413 langkah

Menyelesaikan Pertanyaan N+1 dengan DataLoaders ialah pelajaran Kem Intensif Pembangunan Bahagian Belakang FastAPI percuma di CoddyKit. Ini ialah pelajaran 2 daripada 4. Sebanyak 3 pelajaran dalam laluan pembelajaran ini boleh dibaca sepenuhnya secara percuma — selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan praktikal dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Pelajaran ini merupakan sebahagian daripada laluan pembelajaran Kem Intensif Pembangunan Bahagian Belakang FastAPI, dan kemajuan anda disegerakkan merentas web serta aplikasi CoddyKit. Kursus Kem Intensif Pembangunan Bahagian Belakang FastAPI merangkumi sejumlah 4 pelajaran.

Masalah N+1 dalam GraphQL

GraphQL membolehkan klien meminta data bersarang dalam satu permintaan, seperti senarai posts dan author bagi setiap siaran. Bahayanya tersembunyi dalam penyelesai.

Andaikan anda mengambil 100 siaran dengan 1 query, kemudian menyelesaikan pengarang setiap siaran dengan menjalankan satu query bagi setiap siaran. Itu bersamaan 1 + 100 = 101 query — masalah N+1 yang klasik.

  • 1 query untuk memuatkan senarai (iaitu 1)
  • N query, satu bagi setiap item, untuk memuatkan medan berkaitan (iaitu N)

Pada skala besar, ini memusnahkan kependaman dan membebankan pangkalan data. DataLoader ialah pembaikan standard.

Melihat N+1 dalam Penyelesai Strawberry

Berikut ialah penyelesai Strawberry naif yang mencetuskan N+1. Setiap penyelesai author membuat panggilan pangkalan datanya sendiri.

Jika query memulangkan 50 siaran, penyelesai author ini akan mencetuskan 50 pernyataan SELECT yang berasingan. Query senarai ditambah 50 carian tersebut menghasilkan letupan N+1.

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"])

Idea Teras: Kumpulan dan Cache

DataLoader menyelesaikan N+1 dengan dua teknik:

  • Pengumpulan: bukannya menyelesaikan setiap author_id dengan serta-merta, pemuat mengumpulkan semua kunci yang diminta semasa satu detik gelung peristiwa dan menyelesaikannya bersama-sama dalam satu query berkumpulan (contohnya WHERE id = ANY(...)).
  • Pencachean: dalam satu permintaan, kunci yang sama hanya diambil sekali. Meminta pengarang 7 sepuluh kali menghasilkan satu carian.

Hasilnya: 1 query untuk siaran + 1 query berkumpulan untuk semua pengarang = 2 query, bukannya 101.

Cara Pengumpulan Berfungsi pada Gelung Peristiwa

DataLoader Strawberry bergantung pada gelung peristiwa asyncio. Apabila beberapa penyelesai memanggil loader.load(key), pemuat tidak berjalan serta-merta. Ia merekodkan setiap kunci dan memulangkan awaitable yang belum selesai.

Pada detik berikutnya, pemuat mengambil setiap kunci dalam barisan, memanggil fungsi kelompok anda sekali dengan senarai kunci penuh, kemudian menyelesaikan setiap awaitable dengan hasil yang sepadan.

Inilah sebabnya DataLoader hanya berfungsi dalam kod tak segerak: mekanisme penangguhan bergantung pada penjadualan gelung untuk menghantar kelompok selepas kerja segerak semasa selesai.

Menulis Fungsi Pemuatan Kelompok

Teras DataLoader ialah fungsi kelompok. Ia menerima senarai kunci dan mesti memulangkan senarai hasil dalam susunan yang sama tepat seperti kunci.

Dua peraturan yang tidak boleh dirundingkan:

  • Panjang senarai yang dipulangkan mesti sama dengan panjang kunci.
  • Hasil pada indeks i mesti sepadan dengan keys[i]. Baris yang tiada hendaklah dipetakan kepada None (atau Exception), dan tidak boleh dibuang.

Di bawah, kami memetakan baris mengikut id, kemudian mengeluarkannya semula mengikut susunan kunci.

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]

Penjajaran Susunan Ditunjukkan

Kontrak pengekalan susunan ialah punca pepijat DataLoader yang paling biasa. Berikut ialah simulasi kendiri: baris tiba dalam susunan sewenang-wenangnya daripada pangkalan data, tetapi kita mesti mengembalikannya sejajar dengan kunci yang diminta.

Jalankan ini untuk melihat cara kamus carian bersama pemahaman yang disusun mengikut kunci menjamin penjajaran yang betul walaupun DB mengembalikan baris dalam susunan yang berbeza atau tidak menyertakan kunci yang tiada.

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}")

Mencipta DataLoader dalam Strawberry

Strawberry menyediakan kelas DataLoader. Anda membinanya menggunakan fungsi kelompok anda. Panggilan kepada .load(key) mengembalikan sesuatu yang boleh ditunggu dan akan diselesaikan selepas pengelompokan.

Yang penting, satu tika DataLoader menyimpan cache khusus untuk tika tersebut. Anda mesti mencipta pemuat baharu bagi setiap permintaan supaya data lapuk dan kebocoran merentas pengguna tidak pernah berlaku. Kita akan menyambungkannya seterusnya melalui konteks.

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.

Pemuat bagi Setiap Permintaan melalui Konteks GraphQL

Tempat yang kemas untuk menyimpan pemuat berskop permintaan ialah konteks GraphQL. Dengan FastAPI + Strawberry, anda mengatasi get_context untuk membina pemuat baharu pada setiap permintaan.

Ini menjamin tetingkap pengelompokan dan cache diasingkan kepada satu permintaan — tepat sepanjang tempoh yang anda perlukan.

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")

Menggunakan Pemuat dalam Penyelesai

Sekarang penyelesai author membaca pemuat daripada info.context dan memanggil .load(). Strawberry menyuntik info apabila anda mengisytiharkannya sebagai parameter.

Walaupun penyelesai ini dijalankan sekali bagi setiap kiriman, semua panggilan .load() tersebut dikelompokkan menjadi satu SELECT ... WHERE id = ANY(...) — N+1 telah dihapuskan.

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)

Kelebihan Cache dan Hadnya

Dalam satu permintaan, pemuat menyimpan cache berdasarkan kunci, jadi panggilan load(7) yang berulang hanya mengakses DB sekali. Ini amat berguna untuk pertanyaan kembang keluar apabila pengarang yang sama muncul dalam banyak kiriman.

Perhatikan pertukaran yang terlibat:

  • Cache adalah bagi setiap permintaan mengikut reka bentuk — jangan sekali-kali berkongsi pemuat antara permintaan, kerana anda akan menghidangkan data lapuk.
  • Jika rekod berubah di tengah-tengah permintaan dan anda membacanya semula, anda akan mendapat salinan yang dicache. Panggil loader.clear(key) selepas mutasi untuk membatalkannya.
  • Kunci cache ialah nilai kunci mentah, jadi pastikan kunci boleh dicincang dan konsisten (contohnya sentiasa int, bukan kadangkala str).

Memuatkan Koleksi dan Kunci Tuple

DataLoaders bukan hanya untuk carian satu-ke-satu. Untuk hubungan satu-ke-banyak (comments bagi satu kiriman), fungsi kelompok mengembalikan satu senarai bagi setiap kunci. Kelompokkan baris mengikut kunci asing, kemudian keluarkan satu senarai bagi setiap kunci yang diminta (senarai kosong jika tiada).

Untuk carian gabungan, gunakan tuple yang boleh dicincang sebagai kunci, contohnya (post_id, locale). Pastikan jenisnya kekal supaya caching terus betul.

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]

Semakan Ringkas: Tempoh Hayat DataLoader

Seorang rakan sepasukan mencipta satu DataLoader pada peringkat modul dan menggunakannya semula untuk seluruh aplikasi bagi "menjimatkan memori." Mengapakah ini pilihan yang salah untuk perkhidmatan FastAPI GraphQL berbilang pengguna?

Imbas Kembali: DataLoaders Mengatasi N+1

Anda telah mempelajari cara menghapuskan ledakan pertanyaan N+1 dalam penyelesai Strawberry + FastAPI:

  • N+1 berlaku apabila penyelesai bersarang mengeluarkan satu pertanyaan bagi setiap item induk.
  • DataLoader membaikinya dengan mengelompokkan semua kunci daripada satu detik gelung acara menjadi satu pertanyaan dan mencache kunci yang berulang dalam permintaan tersebut.
  • Fungsi kelompok mesti mengembalikan hasil yang sejajar dengan kunci input, dengan panjang dan susunan yang sama, serta None atau senarai kosong bagi carian yang gagal.
  • Bina pemuat bagi setiap permintaan dalam get_context dan bacanya daripada info.context di dalam penyelesai.
  • Gunakan senarai bagi setiap kunci untuk hubungan satu-ke-banyak dan kunci tuple yang boleh dicincang untuk carian gabungan; panggil clear() selepas mutasi.

Dengan corak ini, pertanyaan GraphQL yang bersarang secara mendalam kekal pantas dan pangkalan data anda kekal stabil.

Percuma untuk bermula

Pelajari Kem Intensif Pembangunan Bahagian Belakang FastAPI dengan tutor kecerdasan buatan — percuma

Tulis dan jalankan kod sebenar dalam pelayar anda, dapatkan bantuan segera daripada tutor kecerdasan buatan yang tersedia 24/7, dan sambung semula dari tempat anda berhenti di web atau dalam aplikasi.

Kursus
21
Pelajaran
84

Soalan Lazim

Adakah pelajaran “Menyelesaikan Pertanyaan N+1 dengan DataLoaders” percuma?

Ya — sebanyak 3 pelajaran dalam laluan pembelajaran Kem Intensif Pembangunan Bahagian Belakang FastAPI, termasuk “Menyelesaikan Pertanyaan N+1 dengan DataLoaders”, boleh dibaca sepenuhnya secara percuma di web ini. Selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan interaktif dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Kursus Kem Intensif Pembangunan Bahagian Belakang FastAPI merangkumi sejumlah 4 pelajaran.

Apakah yang akan saya pelajari dalam “Menyelesaikan Pertanyaan N+1 dengan DataLoaders”?

Kelompokkan dan cache carian pangkalan data menggunakan pemuat data untuk menghapuskan ledakan pertanyaan N+1 dalam penyelesai. Anda berlatih Kem Intensif Pembangunan Bahagian Belakang FastAPI menggunakan kod praktikal yang dijalankan terus dalam pelayar, manakala tutor kecerdasan buatan 24/7 menjawab soalan anda semasa anda mengikuti pelajaran.

Adakah saya memerlukan pengalaman untuk memulakan Kem Intensif Pembangunan Bahagian Belakang FastAPI?

Tiada pengalaman terdahulu diperlukan. Pembelajaran Kem Intensif Pembangunan Bahagian Belakang FastAPI di CoddyKit disusun untuk pelajar daripada peringkat pemula hingga lanjutan, jadi anda boleh bermula di sini atau dari awal dan belajar mengikut kadar anda sendiri. Ini ialah pelajaran 2 daripada 4.

Berapa lamakah pelajaran “Menyelesaikan Pertanyaan N+1 dengan DataLoaders” diambil?

Kebanyakan pelajaran CoddyKit mengambil masa kira-kira 5–10 minit. Setiap pelajaran ringkas dan interaktif, jadi anda boleh membuat kemajuan secara berterusan dan menyambung tepat dari tempat anda berhenti di web atau aplikasi.

Bolehkah saya menulis dan menjalankan kod dalam pelajaran Kem Intensif Pembangunan Bahagian Belakang FastAPI ini?

Ya. Setiap pelajaran Kem Intensif Pembangunan Bahagian Belakang FastAPI menyertakan penyunting kod terbina dalam, jadi anda boleh menulis dan menjalankan kod sebenar terus dalam pelayar serta menerima maklum balas kecerdasan buatan serta-merta — tanpa memerlukan persediaan setempat.

Semua pelajaran dalam kursus ini

  1. Mentakrifkan Jenis, Pertanyaan dan Mutasi
  2. Menyelesaikan Pertanyaan N+1 dengan DataLoaders
  3. Langganan GraphQL Masa Nyata
  4. Analisis Kos Pertanyaan dan Had Kedalaman
← Kembali ke Kem Intensif Pembangunan Bahagian Belakang FastAPI