FastAPI Backend Development Bootcamp · บทเรียน

พารามิเตอร์การกรองและการเรียงลำดับแบบไดนามิก

สร้างโมเดลพารามิเตอร์คำค้นหาที่นำกลับมาใช้ซ้ำได้สำหรับการกรอง การเรียงลำดับ และการเลือกฟิลด์พร้อมการตรวจสอบ

บทเรียน 3 จาก 413 ขั้นตอน

พารามิเตอร์การกรองและการเรียงลำดับแบบไดนามิก เป็นบทเรียน FastAPI Backend Development Bootcamp ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน FastAPI Backend Development Bootcamp และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส FastAPI Backend Development Bootcamp มีบทเรียนทั้งหมด 4 บทเรียน

เหตุใดจึงต้องใช้พารามิเตอร์คำสั่งแบบไดนามิก

ปลายทางสำหรับรายการในโลกจริงแทบไม่ส่งคืนข้อมูลทั้งหมด ไคลเอ็นต์ต้องการกรอง (เฉพาะผู้ใช้ที่ใช้งานอยู่) เรียงลำดับ (รายการใหม่สุดก่อน) และเลือกฟิลด์ (เฉพาะ id และชื่อ) หากเขียนกำหนดทุกชุดความเป็นไปได้ไว้ตายตัว จำนวนเส้นทางจะเพิ่มขึ้นอย่างรวดเร็ว

แนวทางที่สะอาดกว่าคือสร้างพารามิเตอร์คำสั่งเหล่านี้เป็นออบเจ็กต์ที่นำกลับมาใช้ใหม่ได้และผ่านการตรวจสอบค่า แล้วฉีดเข้าไปในปลายทางหลายแห่ง ในบทเรียนนี้เราจะสร้าง:

  • โมเดลตัวกรองที่แปลงพารามิเตอร์คำสั่งเป็นเงื่อนไขที่ปลอดภัย
  • ตัวแยกวิเคราะห์การเรียงลำดับพร้อมรายการอนุญาตของฟิลด์และทิศทาง
  • กลไกการเลือกฟิลด์เพื่อลดขนาดข้อมูลผลลัพธ์

ทุกอย่างขับเคลื่อนด้วย dependency ของ FastAPI จึงยังคงเป็นไปตามหลัก DRY และทดสอบได้

การรวบรวมตัวกรองด้วยคลาส dependency

คลาสที่มี __init__ ซึ่งรับพารามิเตอร์ Query จะกลายเป็น dependency ที่นำกลับมาใช้ใหม่ได้ FastAPI จะอ่านพารามิเตอร์แต่ละตัวจาก URL และจัดทำเอกสารไว้ใน OpenAPI โดยอัตโนมัติ

ใช้ Optional[...] = None เพื่อให้ตัวกรองเป็นตัวเลือก: หากไม่มีพารามิเตอร์ ก็หมายความว่า “ไม่ต้องกรองคอลัมน์นี้”

from typing import Optional
from fastapi import Query

class UserFilterParams:
    def __init__(
        self,
        status: Optional[str] = Query(None, description="active | inactive"),
        min_age: Optional[int] = Query(None, ge=0, le=150),
        search: Optional[str] = Query(None, min_length=2, max_length=50),
    ):
        self.status = status
        self.min_age = min_age
        self.search = search

# Usage:
# @app.get('/users')
# def list_users(filters: UserFilterParams = Depends()):
#     ...

การตรวจสอบค่าตัวกรองด้วย Enum

ตัวกรองข้อความอิสระอย่าง status=foo เปิดทางให้ข้อมูลนำเข้าที่ไม่ถูกต้องผ่านเข้ามาได้ ให้จำกัดค่าด้วย Enum ที่อิงจาก str: FastAPI จะปฏิเสธค่าที่อยู่นอกชุดที่อนุญาต และแสดงรายการแบบเลื่อนลงในเอกสาร

นี่คือแนวป้องกันด่านแรก ค่าตัวกรองที่ไม่ถูกต้องจะได้รับข้อผิดพลาด 422 ที่ชัดเจน แทนที่จะรั่วไหลไปยังชั้นคำสั่งค้นข้อมูล

from enum import Enum
from typing import Optional
from fastapi import Query

class UserStatus(str, Enum):
    active = "active"
    inactive = "inactive"
    pending = "pending"

class UserFilterParams:
    def __init__(
        self,
        status: Optional[UserStatus] = Query(None),
        min_age: Optional[int] = Query(None, ge=0, le=150),
    ):
        self.status = status
        self.min_age = min_age

การเปลี่ยนตัวกรองให้เป็นเพรดิเคต

แยกชั้น HTTP ออกจากชั้นข้อมูลออกจากกัน dependency มีหน้าที่เพียงรวบรวมและตรวจสอบค่า ส่วนตัวช่วยขนาดเล็กจะแปลงออบเจ็กต์ที่มีข้อมูลแล้วให้เป็นเพรดิเคตสำหรับกรองจริง

ต่อไปนี้เป็นเวอร์ชันที่ไม่ขึ้นกับเฟรมเวิร์ก ซึ่งคุณสามารถเรียกใช้กับดิกชันนารีทั่วไปได้ รูปแบบเดียวกันนี้สามารถนำไปใช้กับการเรียก .filter() ของ SQLAlchemy ได้อย่างราบรื่นในภายหลัง

USERS = [
    {"id": 1, "name": "Ada", "status": "active", "age": 36},
    {"id": 2, "name": "Linus", "status": "inactive", "age": 54},
    {"id": 3, "name": "Grace", "status": "active", "age": 41},
]

def apply_filters(rows, status=None, min_age=None, search=None):
    result = rows
    if status is not None:
        result = [r for r in result if r["status"] == status]
    if min_age is not None:
        result = [r for r in result if r["age"] >= min_age]
    if search is not None:
        result = [r for r in result if search.lower() in r["name"].lower()]
    return result

print(apply_filters(USERS, status="active", min_age=40))

การแยกวิเคราะห์พารามิเตอร์การเรียงลำดับ

สัญญาที่ใช้กันทั่วไปคือ ?sort=-created_at,name ซึ่งเป็นรายการคั่นด้วยจุลภาค โดยเครื่องหมาย - นำหน้าหมายถึงเรียงจากมากไปน้อย ให้แยกวิเคราะห์เป็นทูเพิล (field, direction)

อย่าเชื่อชื่อฟิลด์จากไคลเอ็นต์โดยไม่ตรวจสอบ ตรวจสอบแต่ละฟิลด์กับรายการอนุญาต เพื่อป้องกันไม่ให้ผู้ใช้เรียงลำดับหรือตรวจสอบคอลัมน์ใด ๆ ตามใจ

ALLOWED_SORT = {"created_at", "name", "age", "id"}

def parse_sort(sort_param):
    parsed = []
    for token in sort_param.split(","):
        token = token.strip()
        if not token:
            continue
        descending = token.startswith("-")
        field = token[1:] if descending else token
        if field not in ALLOWED_SORT:
            raise ValueError(f"Cannot sort by '{field}'")
        parsed.append((field, "desc" if descending else "asc"))
    return parsed

print(parse_sort("-created_at,name"))
print(parse_sort("age"))

Dependency สำหรับการเรียงลำดับที่นำกลับมาใช้ใหม่ได้

ห่อตัวแยกวิเคราะห์ไว้ใน dependency เพื่อให้ปลายทางรายการทุกแห่งใช้สัญญาและการตรวจสอบการเรียงลำดับแบบเดียวกัน การส่ง HTTPException(422) เมื่อฟิลด์ไม่ถูกต้องจะให้ข้อผิดพลาดที่แม่นยำและเครื่องอ่านได้แก่ไคลเอ็นต์

การส่งรายการอนุญาตเข้าไปทำให้ dependency นี้นำกลับมาใช้ซ้ำกับทรัพยากรที่มีคอลัมน์สำหรับเรียงลำดับแตกต่างกันได้

from typing import Optional
from fastapi import Query, HTTPException

def sort_dependency(allowed: set):
    def _parse(sort: Optional[str] = Query(None, example="-created_at,name")):
        if not sort:
            return []
        parsed = []
        for token in sort.split(","):
            token = token.strip()
            if not token:
                continue
            desc = token.startswith("-")
            field = token[1:] if desc else token
            if field not in allowed:
                raise HTTPException(422, f"Invalid sort field: {field}")
            parsed.append((field, "desc" if desc else "asc"))
        return parsed
    return _parse

# @app.get('/users')
# def list_users(sort=Depends(sort_dependency({'created_at','name'}))):
#     ...

การเรียงลำดับหลายคีย์ในหน่วยความจำ

ต้องใช้คีย์สำหรับเรียงลำดับหลายตัวตามลำดับ วิธีที่คงที่คือเรียงด้วยคีย์ที่มีนัยสำคัญน้อยที่สุดก่อน แล้วทำย้อนกลับ เพราะ sorted ของ Python รักษาลำดับเดิมเมื่อค่าเท่ากัน

ตัวอย่างแบบทำงานได้ในตัวเองนี้ให้ผลลัพธ์สอดคล้องกับสิ่งที่ฐานข้อมูลจะสร้างจาก ORDER BY a, b DESC

ROWS = [
    {"name": "Ada", "age": 36},
    {"name": "Grace", "age": 36},
    {"name": "Linus", "age": 54},
]

def apply_sort(rows, sort_keys):
    result = list(rows)
    for field, direction in reversed(sort_keys):
        result.sort(key=lambda r: r[field], reverse=(direction == "desc"))
    return result

ordered = apply_sort(ROWS, [("age", "desc"), ("name", "asc")])
for r in ordered:
    print(r)

การเลือกฟิลด์ (ชุดฟิลด์แบบกระชับ)

หากต้องการลดขนาดข้อมูล ให้รองรับ ?fields=id,name ไคลเอ็นต์จะเลือกคีย์ที่จะส่งกลับได้ แต่เช่นเคย ต้องตรวจสอบกับรายการอนุญาตของฟิลด์ที่เปิดเผยได้ เพื่อไม่ให้มีการร้องขอคอลัมน์ภายใน เช่น password_hash

การเลือกเป็นขั้นตอนการฉายข้อมูลที่ทำหลังจากการกรองและการเรียงลำดับ และทำทันทีก่อนการแปลงเป็นข้อมูลอนุกรม

EXPOSABLE = {"id", "name", "status", "age"}

def select_fields(rows, fields_param):
    if not fields_param:
        return rows
    requested = {f.strip() for f in fields_param.split(",") if f.strip()}
    invalid = requested - EXPOSABLE
    if invalid:
        raise ValueError(f"Unknown fields: {sorted(invalid)}")
    return [{k: r[k] for k in requested if k in r} for r in rows]

data = [{"id": 1, "name": "Ada", "status": "active", "age": 36}]
print(select_fields(data, "id,name"))

การรวมการกรอง การเรียงลำดับ การเลือก และการแบ่งหน้า

ลำดับของกระบวนการมีความสำคัญต่อความถูกต้องและประสิทธิภาพ: กรองก่อนเพื่อลดชุดข้อมูล จากนั้นเรียงลำดับ แล้วแบ่งหน้า (ตัดข้อมูล) และสุดท้ายเลือกฟิลด์เฉพาะในหน้าที่จะส่งคืน

การเลือกฟิลด์ก่อนแบ่งหน้าจะยังคงต้องสแกนข้อมูลทั้งหมด และการแบ่งหน้าก่อนเรียงลำดับจะทำให้ได้หน้าที่ไม่ถูกต้อง

def list_resource(rows, *, filters, sort_keys, fields, offset, limit,
                  apply_filters, apply_sort, select_fields):
    rows = apply_filters(rows, **filters)
    rows = apply_sort(rows, sort_keys)
    total = len(rows)
    page = rows[offset: offset + limit]
    page = select_fields(page, fields)
    return {"total": total, "items": page,
            "offset": offset, "limit": limit}

# In FastAPI each piece is a Depends(); the route just calls list_resource.

การประกอบ dependency เป็นออบเจ็กต์คำสั่งเดียว

แทนที่จะส่ง dependency สี่ตัวแยกกันเข้าไปในทุกเส้นทาง ให้ประกอบรวมกัน dependency ตัวห่อหุ้มสามารถส่งคืนออบเจ็กต์ที่เป็นระเบียบหนึ่งตัว ซึ่งเก็บตัวกรอง คีย์การเรียงลำดับ ฟิลด์ และการแบ่งหน้าไว้

วิธีนี้ทำให้ลายเซ็นของเส้นทางสั้นลง และมีจุดเดียวสำหรับปรับขยายสัญญาของคำสั่ง

from dataclasses import dataclass
from typing import Optional
from fastapi import Depends, Query

@dataclass
class ListQuery:
    filters: object
    sort: list
    fields: Optional[str]
    offset: int
    limit: int

def list_query(
    filters: "UserFilterParams" = Depends(),
    sort: list = Depends(sort_dependency({"created_at", "name"})),
    fields: Optional[str] = Query(None),
    offset: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
) -> ListQuery:
    return ListQuery(filters, sort, fields, offset, limit)

# @app.get('/users')
# def list_users(q: ListQuery = Depends(list_query)):
#     ...

การจัดทำเอกสารและกำหนดค่าเริ่มต้นให้สัญญา

สัญญาของคำค้นที่ดีควร อธิบายตัวเองได้ และ ปลอดภัยตั้งแต่ค่าเริ่มต้น:

  • กำหนด description และ example ให้กับ Query ทุกตัว เพื่อให้เอกสาร OpenAPI อธิบายไวยากรณ์ได้
  • จำกัดค่า limit ด้วย le=100 เพื่อไม่ให้ไคลเอ็นต์ร้องขอข้อมูลเป็นล้านแถว
  • เลือกการเรียงลำดับเริ่มต้นที่เหมาะสม เช่น ใหม่ที่สุดก่อน เพื่อให้ผลลัพธ์ แน่นอน ในทุกหน้า
  • ปฏิเสธฟิลด์หรือคีย์การเรียงลำดับที่ไม่รู้จักด้วย 422 แทนการละเว้นโดยไม่แจ้งให้ทราบ

การจัดลำดับให้แน่นอนเป็นสิ่งสำคัญอย่างยิ่ง หากไม่มีการเรียงลำดับที่คงที่ การแบ่งหน้าอาจแสดงแถวซ้ำหรือข้ามบางแถวระหว่างคำขอได้

ตรวจสอบด่วน: ลำดับของไปป์ไลน์

คุณเปิดให้ใช้งาน GET /products ซึ่งรองรับการกรอง การเรียงลำดับ การแบ่งหน้า และชุดฟิลด์แบบเลือกบางส่วน ควรใช้การดำเนินการเหล่านี้ตามลำดับใดเพื่อส่งคืนหน้าที่ถูกต้องอย่างมีประสิทธิภาพ

สรุป

คุณสร้างชั้นคำค้นที่ใช้ซ้ำได้และผ่านการตรวจสอบสำหรับจุดปลายรายการของ FastAPI:

  • ตัวกรอง ในรูปคลาส dependency พร้อมพารามิเตอร์ Optional และการตรวจสอบด้วย Enum/ข้อจำกัด
  • การเรียงลำดับ แยกวิเคราะห์จากไวยากรณ์ -field,field โดยตรวจสอบกับรายการที่อนุญาต และส่งข้อผิดพลาด 422 เมื่อพบฟิลด์ที่ไม่รู้จัก
  • การเลือกฟิลด์ (ชุดฟิลด์แบบเลือกบางส่วน) จำกัดไว้เฉพาะรายการที่อนุญาตให้เปิดเผยได้ เพื่อปกป้องคอลัมน์ภายใน
  • dependency ListQuery ที่ประกอบขึ้นเพื่อให้ลายเซ็นของเส้นทางดูสะอาด

โปรดจำไปป์ไลน์: กรอง → เรียงลำดับ → แบ่งหน้า → เลือก โดยใช้การเรียงลำดับเริ่มต้นที่แน่นอนเสมอ เพื่อให้การแบ่งหน้าสอดคล้องกัน รูปแบบเหล่านี้นำไปใช้กับคำค้นของ SQLAlchemy ได้โดยตรง เมื่อคุณเปลี่ยนจากข้อมูลในหน่วยความจำไปใช้ฐานข้อมูลจริง

เริ่มต้นได้ฟรี

เรียนรู้ FastAPI Backend Development Bootcamp ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
21
บทเรียน
84

คำถามที่พบบ่อย

บทเรียน “พารามิเตอร์การกรองและการเรียงลำดับแบบไดนามิก” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “พารามิเตอร์การกรองและการเรียงลำดับแบบไดนามิก” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส FastAPI Backend Development Bootcamp ให้อัปเกรดเป็น CoddyKit PRO คอร์ส FastAPI Backend Development Bootcamp มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “พารามิเตอร์การกรองและการเรียงลำดับแบบไดนามิก”

สร้างโมเดลพารามิเตอร์คำค้นหาที่นำกลับมาใช้ซ้ำได้สำหรับการกรอง การเรียงลำดับ และการเลือกฟิลด์พร้อมการตรวจสอบ คุณปฏิบัติ FastAPI Backend Development Bootcamp ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน FastAPI Backend Development Bootcamp หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน FastAPI Backend Development Bootcamp บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “พารามิเตอร์การกรองและการเรียงลำดับแบบไดนามิก” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน FastAPI Backend Development Bootcamp นี้ได้ไหม

ได้ บทเรียน FastAPI Backend Development Bootcamp ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การกำหนดเวอร์ชันด้วย URL ส่วนหัว และชนิดสื่อ
  2. การแบ่งหน้าแบบเคอร์เซอร์เทียบกับออฟเซ็ตในระบบขนาดใหญ่
  3. พารามิเตอร์การกรองและการเรียงลำดับแบบไดนามิก
  4. การออกแบบซองหุ้มการตอบกลับที่เสถียร
← กลับไปที่ FastAPI Backend Development Bootcamp