نظام حقن التبعيات — التصميم الأساسي لـ FastAPI
حقن التبعيات مثل واجهات قطع الليغو—كل قطعة تهتم فقط بما يجب أن تقدمه ولا تحتاج لمعرفة من يستخدمها. اتصالات قاعدة البيانات والمستخدم الحالي وفحوصات الصلاحيات يتم حلها تلقائيًا عبر طبقات متداخلة.
1. ما ستتعلمه
- أساسيات
Depends(): تبعيات الدوال، تبعيات الأصناف، التبعيات الفرعية المتداخلة - دورة حياة التبعية: مستوى الطلب مقابل مستوى التطبيق (تبعيات
yieldوتنظيف الموارد) - التبعيات العامة وتبعيات مجموعات التوجيه:
dependencies=[Depends(...)] - تغطية التبعيات والاختبار: نصائح عملية مع
app.dependency_overrides - سيناريو Alice: تبعية جلسة قاعدة البيانات في PriceTracker، وتبعية المستخدم الحالي، وتبعية فحص الصلاحيات—سلسلة DI متداخلة من ثلاث طبقات
2. قصة Alice الحقيقية
(1) المشكلة: كل نقطة نهاية تسترجع البيانات من قاعدة البيانات ومعلومات المستخدم بشكل متكرر
يحتوي PriceTracker الخاص بـ Alice على 20 نقطة نهاية، كل منها يتطلب فتح اتصال قاعدة بيانات والتحقق من رمز JWT واسترجاع المستخدم الحالي وفحص الصلاحيات. تنسخ وتلصق نفس 15 سطرًا من كود التهيئة في كل دالة نقطة نهاية، لذلك إذا تغيرت طريقة اتصال قاعدة البيانات، يجب عليها إجراء 20 تعديلًا منفصلًا.
(2) حلول حقن التبعيات
يستخرج حقن التبعيات في FastAPI المنطق المتكرر إلى دوال تبعية قابلة لإعادة الاستخدام. تحتاج نقاط النهاية فقط إلى تعريف db = Depends(get_db)، ويقوم FastAPI بالحل والحقن تلقائيًا، بما في ذلك الحل التكراري للتبعيات الفرعية.
from fastapi import Depends
async def get_db():
db = Database()
yield db
db.close()
async def get_current_user(token: str, db=Depends(get_db)):
return verify_token(token, db)
@app.get("/products")
async def list_products(user=Depends(get_current_user), db=Depends(get_db)):
# user و db يتم حقنهما تلقائيًا
...
(3) العائد
اتصالات قاعدة البيانات ومصادقة المستخدم وفحوصات الصلاحيات—التي كانت تتطلب 15 سطرًا من الكود لكل من نقاط النهاية العشرين—تم دمجها في ثلاث دوال تبعية، بحيث يصبح التغيير في مكان واحد ساريًا على المستوى العام. أثناء الاختبار، ببساطة استخدم app.dependency_overrides[get_db] = lambda: mock_db لاستبدال معلومات قاعدة البيانات لجميع نقاط النهاية بسطر واحد من الكود.
3. أساسيات Depends()
(1) تبعيات الدوال
(1) ▶مثال: تبعيات الدوال البسيطة
from fastapi import FastAPI, Depends
app = FastAPI()
def common_parameters(
q: str | None = None,
skip: int = 0,
limit: int = 100,
):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/products")
async def list_products(commons: dict = Depends(common_parameters)):
return commons
@app.get("/prices")
async def list_prices(commons: dict = Depends(common_parameters)):
return commons
الناتج:
# تم تعريف الدالة بنجاح
(2) ▶مثال: تبعيات الأصناف
from fastapi import FastAPI, Depends, Query
class CommonQueryParams:
def __init__(
self,
q: str | None = Query(None),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=200),
):
self.q = q
self.skip = skip
self.limit = limit
app = FastAPI()
@app.get("/products")
async def list_products(commons: CommonQueryParams = Depends(CommonQueryParams)):
return {"q": commons.q, "skip": commons.skip, "limit": commons.limit}
الناتج:
# تم تعريف الدالة بنجاح
(2) تبعيات الدوال مقابل تبعيات الأصناف
| البُعد | تبعية الدالة | تبعية الصنف |
|---|---|---|
| طريقة التعريف | def get_xxx() |
class Xxx: + __init__ |
| حالات الاستخدام | منطق بسيط، استدعاء واحد | يتطلب حالة، طرق متعددة |
| تحليل المعاملات | تحليل تلقائي لمعاملات الدالة | تحليل تلقائي لمعاملات __init__ |
| قابلية إعادة الاستخدام | عالية | متوسطة |
| مستوى التوصية | الخيار الأول | استخدم في السيناريوهات المعقدة |
4. التبعيات الفرعية المتداخلة
(1) شجرة حل التبعيات
graph TD
A[get_current_user] --> B[get_db]
A --> C[get_token_from_header]
C --> D[OAuth2PasswordBearer]
E[require_admin] --> A
E --> F[check_subscription]
style A fill:#e1f5fe
style E fill:#fff3e0
يحل FastAPI شجرة التبعيات بشكل تكراري: require_admin → get_current_user → get_db، مع إنشاء مثيل لكل تبعية مرة واحدة فقط (مخزنة مؤقتًا ضمن نفس الطلب).
(1) ▶مثال: تبعيات فرعية متداخلة—DB → المستخدم
from fastapi import FastAPI, Depends, HTTPException, Header
app = FastAPI()
# المستوى 1: جلسة قاعدة البيانات
async def get_db():
db = {"connection": "active"}
try:
yield db
finally:
db["connection"] = "closed"
# المستوى 2: الحصول على المستخدم الحالي (يعتمد على DB)
async def get_current_user(
authorization: str = Header(...),
db: dict = Depends(get_db),
):
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid token")
token = authorization.replace("Bearer ", "")
# مبسط: في الإنتاج، تحقق من JWT
user = {"id": 1, "username": "alice", "role": "admin"}
return user
# المستوى 3: يتطلب صلاحيات المسؤول (يعتمد على المستخدم الحالي)
async def require_admin(user: dict = Depends(get_current_user)):
if user["role"] != "admin":
raise HTTPException(status_code=403, detail="Admin required")
return user
@app.get("/admin/stats")
async def admin_stats(admin: dict = Depends(require_admin)):
return {"admin": admin["username"], "total_products": 1000000}
الناتج:
# تم تعريف الدالة بنجاح
5. دورة حياة التبعية و yield
(1) مستوى الطلب مقابل مستوى التطبيق
| دورة الحياة | طريقة التعريف | النطاق | الاستخدامات النموذجية |
|---|---|---|---|
| مستوى الطلب | def dep(): yield x; cleanup |
يتم إنشاؤه وإتلافه في كل طلب | جلسة قاعدة البيانات، اتصال Redis |
| مستوى التطبيق | @app.on_event("startup") |
يتم إنشاؤه عند بدء التطبيق | تهيئة المحرك، تجمع الاتصالات |
(1) ▶مثال: تبعيات yield وتنظيف الموارد
from fastapi import FastAPI, Depends
app = FastAPI()
# تبعية بنطاق الطلب مع التنظيف
async def get_db_session():
# الإعداد: إنشاء الجلسة
session = {"id": "session-123", "active": True}
print(f"DB session opened: {session['id']}")
try:
yield session # يتم حقن هذا في نقطة النهاية
finally:
# التنظيف: إغلاق الجلسة (يعمل بعد الاستجابة)
session["active"] = False
print(f"DB session closed: {session['id']}")
@app.get("/products")
async def list_products(db: dict = Depends(get_db_session)):
print(f"Using DB session: {db['id']}")
return {"session_id": db["id"]}
الناتج (سجلات الخادم):
DB session opened: session-123
Using DB session: session-123
DB session closed: session-123
6. التبعيات العامة وتبعيات مجموعات التوجيه
(1) تعريفات التبعيات بنطاقات مختلفة
(1) ▶مثال: تبعيات مجموعة التوجيه
from fastapi import FastAPI, Depends, APIRouter, Header, HTTPException
async def verify_api_key(x_api_key: str = Header(...)):
if x_api_key != "secret-key-123":
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
app = FastAPI()
# المسارات العامة - لا حاجة لمصادقة
public_router = APIRouter()
@public_router.get("/health")
async def health():
return {"status": "healthy"}
# المسارات المحمية - مفتاح API مطلوب لجميع المسارات في هذه المجموعة
protected_router = APIRouter(dependencies=[Depends(verify_api_key)])
@protected_router.get("/products")
async def list_products():
return [{"id": 1, "name": "Widget"}]
@protected_router.post("/products")
async def create_product():
return {"id": 2, "name": "New Product"}
# تسجيل الموجهات
app.include_router(public_router)
app.include_router(protected_router, prefix="/api/v1")
الناتج:
# تم تعريف الدالة بنجاح
| نطاق التبعية | موقع التعريف | نطاق التأثير |
|---|---|---|
| مستوى نقطة النهاية | @app.get("/", dependencies=[...]) |
نقطة نهاية واحدة |
| مستوى مجموعة التوجيه | APIRouter(dependencies=[...]) |
جميع نقاط النهاية في مجموعة التوجيه |
| المستوى العام | FastAPI(dependencies=[...]) |
جميع نقاط نهاية التطبيق |
7. تغطية التبعيات والاختبار
(1) dependency_overrides
أثناء الاختبار، من الضروري استبدال تبعيات الإنتاج (مثل اتصالات قاعدة البيانات وواجهات API الخارجية)؛ يسمح لك app.dependency_overrides باستبدال تطبيقات التبعية دون تعديل الكود المصدري.
(1) ▶مثال: استبدال تبعيات قاعدة البيانات في الاختبارات
from fastapi.testclient import TestClient
from fastapi import FastAPI, Depends
app = FastAPI()
# التبعية الحقيقية
async def get_db():
return {"type": "postgresql", "host": "prod-db"}
# تبعية وهمية للاختبار
def get_mock_db():
return {"type": "sqlite", "host": "memory"}
@app.get("/db-info")
async def db_info(db: dict = Depends(get_db)):
return db
# في الاختبارات:
def test_db_info():
app.dependency_overrides[get_db] = get_mock_db
client = TestClient(app)
response = client.get("/db-info")
assert response.json() == {"type": "sqlite", "host": "memory"}
# تنظيف
app.dependency_overrides.clear()
الناتج:
# تم تعريف الدالة بنجاح
❓أسئلة شائعة
yield؟try/finally).dependency_overrides على اختبارات أخرى؟app. تأكد من استدعاء app.dependency_overrides.clear() أو استخدام fixtures لإدارته بعد انتهاء الاختبار.📖ملخص
- يستخرج
Depends()المنطق المتكرر إلى دوال أو أصناف مساعدة قابلة لإعادة الاستخدام؛ تحتاج نقاط النهاية فقط إلى تعريفها ولا تحتاج للقلق بشأن التنفيذ - حل تكراري تلقائي للتبعيات الفرعية؛ تخزين مؤقت ضمن نفس الطلب لتجنب العمليات المكررة
- ينفذ
yieldإدارة دورة حياة التبعيات على مستوى الطلب لضمان إقران إنشاء الموارد وتنظيفها - تبعيات مجموعات التوجيه (
APIRouter(dependencies=[...])) تسمح لمجموعة من نقاط النهاية بمشاركة منطق المصادقة والتحقق app.dependency_overridesهو أداة اختبار قوية تستبدل التبعيات الفعلية لجميع نقاط النهاية بسطر واحد من الكود
📝تمارين
- مشكلة أساسية (الصعوبة ⭐): أنشئ دالة مساعدة
get_dbتُرجع قاموسًا يحاكي اتصال قاعدة بيانات، وحقنها واستخدمها في كلتا نقطتي النهاية. تلميح:db: dict = Depends(get_db) - مشكلة متقدمة (الصعوبة ⭐⭐): نفّذ مستويين من التبعيات المتداخلة:
get_db→get_current_user(قراءة رأس Authorization من الطلب)، وحقن معلومات المستخدم الحالي في نقطة النهاية/me. تلميح:user: dict = Depends(get_current_user) - تحدي (الصعوبة ⭐⭐⭐): نفّذ سلسلة DI من ثلاث طبقات لـ PriceTracker:
get_db(yield + تنظيف) →get_current_user(التحقق من الرمز) →require_subscription("pro")(فحص مستوى الاشتراك)، واستبدل تبعية DB بـdependency_overridesأثناء الاختبار. تلميح: تبعيةyield+app.dependency_overrides[get_db] = mock_fn
---|



