404 Not Found

404 Not Found


nginx

تنفيذ CRUD الكامل — من مخزن البيانات إلى نقطة نهاية API

CRUD مثل العمليات الأربع الأساسية للمستودع—الاستلام (Create)، وجرد المخزون (Read)، ونقل البضائع (Update)، والشحن (Delete). نموذج المستودع يضمن توحيد العمليات، مما يمنع الارتباك الناتج عن مديري مستودعات مختلفين.

1. ما ستتعلمه


2. القصة الحقيقية لـ Alice

(1) نقطة الألم: SQL مبعثرة في دوال التوجيه

في كود PriceTracker الخاص بـ Alice، تُكتب استعلامات SQL مباشرة داخل دوال معالج التوجيه، وهناك 30 حالة مكررة لمنطق select(Product).where(...) عبر 20 نقطة نهاية. عندما احتاجت إلى إضافة شرط "فلتر الحذف الناعم" لجميع استعلامات المنتجات، كان على Alice إجراء تغييرات في 15 مكانًا؛ تفويت اثنين فقط منهما تسبب في ظهور المنتجات المحذوفة في النتائج.

(2) حلول نموذج المستودع

غلف عمليات قاعدة البيانات داخل فئة Repository؛ يجب على دوال التوجيه استدعاء دوال Repository فقط. لإضافة فلتر عام، تحتاج فقط إلى إجراء تغيير واحد في Repository.

PYTHON
class ProductRepository:
    async def get_all(self, db: AsyncSession, filters: ProductFilter) -> list[Product]:
        stmt = select(Product).where(Product.deleted == False)
        # إضافة فلاتر من نموذج Pydantic
        if filters.category:
            stmt = stmt.where(Product.category == filters.category)
        result = await db.execute(stmt)
        return result.scalars().all()

(3) العائد

تركز منطق SQL في Repository، مما يجعل دالة التوجيه أكثر إيجازًا (5 أسطر بدلاً من 15). يمكن تعديل شروط الفلتر العامة في مكان واحد لتسري مفعولها على مستوى النظام، لذا لن يرى Bob في الواجهة الأمامية المنتجات المحذوفة بعد الآن.


3. البنية الطبقية ونماذج مستودع البيانات

(1) بنية رباعية الطبقات

100%
flowchart TD
    Router[طبقة التوجيه] -->|استدعاء| Service[طبقة الخدمة]
    Service -->|استدعاء| Repository[طبقة المستودع]
    Repository -->|استعلام| SQLAlchemy[SQLAlchemy ORM]
    SQLAlchemy -->|SQL| Database[(PostgreSQL)]
    
    style Router fill:#e3f2fd
    style Service fill:#fff3e0
    style Repository fill:#e8f5e9
    style SQLAlchemy fill:#fce4ec
المستوى المسؤوليات أمثلة
التوجيه التحقق من المعلمات، تنسيق الاستجابة @app.get("/products")
الخدمة منطق الأعمال، تنسيق المعاملات ProductService.create_with_price()
المستودع الوصول للبيانات، تغليف SQL ProductRepository.get_by_id()
ORM تعيين الكائنات العلائقية select(Product).where(...)

(1) ▶ مثال: ProductRepository

PYTHON
from sqlalchemy import select, update, delete
from sqlalchemy.ext.asyncio import AsyncSession

class ProductRepository:
    def __init__(self, db: AsyncSession):
        self.db = db

    async def get_by_id(self, product_id: int) -> Product | None:
        stmt = select(Product).where(Product.id == product_id)
        result = await self.db.execute(stmt)
        return result.scalar_one_or_none()

    async def get_all(
        self,
        category: str | None = None,
        skip: int = 0,
        limit: int = 20,
    ) -> list[Product]:
        stmt = select(Product).offset(skip).limit(limit)
        if category:
            stmt = stmt.where(Product.category == category)
        result = await self.db.execute(stmt)
        return list(result.scalars().all())

    async def create(self, product: ProductCreate) -> Product:
        db_product = Product(**product.model_dump())
        self.db.add(db_product)
        await self.db.flush()
        await self.db.refresh(db_product)
        return db_product

    async def update(self, product_id: int, data: dict) -> Product | None:
        stmt = (
            update(Product)
            .where(Product.id == product_id)
            .values(**data)
            .returning(Product)
        )
        result = await self.db.execute(stmt)
        return result.scalar_one_or_none()

    async def delete(self, product_id: int) -> bool:
        stmt = delete(Product).where(Product.id == product_id)
        result = await self.db.execute(stmt)
        return result.rowcount > 0

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: PriceRepository

PYTHON
class PriceRepository:
    def __init__(self, db: AsyncSession):
        self.db = db

    async def get_by_product(
        self,
        product_id: int,
        min_price: float | None = None,
        max_price: float | None = None,
        skip: int = 0,
        limit: int = 50,
    ) -> list[Price]:
        stmt = (
            select(Price)
            .where(Price.product_id == product_id)
            .order_by(Price.recorded_at.desc())
            .offset(skip)
            .limit(limit)
        )
        if min_price is not None:
            stmt = stmt.where(Price.price >= min_price)
        if max_price is not None:
            stmt = stmt.where(Price.price <= max_price)
        result = await self.db.execute(stmt)
        return list(result.scalars().all())

    async def bulk_create(self, prices: list[PriceCreate]) -> list[Price]:
        db_prices = [Price(**p.model_dump()) for p in prices]
        self.db.add_all(db_prices)
        await self.db.flush()
        return db_prices

الناتج:

TEXT
# تم تعريف الدالة بنجاح

4. التقسيم والفرز

(1) نموذج استعلام Pydantic

غلف معلمات التقسيم في نموذج Pydantic لتجنب الاضطرار إلى تعريف معلمات الاستعلام بشكل متكرر في كل نقطة نهاية.

(1) ▶ مثال: نموذج الاستعلام المقسم

PYTHON
from pydantic import BaseModel, Field
from typing import Optional

class PaginationParams(BaseModel):
    skip: int = Field(0, ge=0, description="عدد السجلات للتخطي")
    limit: int = Field(20, ge=1, le=100, description="الحد الأقصى للسجلات المرجعة")

class ProductFilter(PaginationParams):
    category: Optional[str] = Field(None, max_length=100)
    min_price: Optional[float] = Field(None, ge=0, description="الحد الأدنى للسطح بالدولار")
    max_price: Optional[float] = Field(None, ge=0, description="الحد الأقصى للسطح بالدولار")
    sort_by: str = Field("name", pattern="^(name|base_price|created_at)$")
    sort_order: str = Field("asc", pattern="^(asc|desc)$")

الناتج:

TEXT
# التنفيذ ناجح

(2) ▶ مثال: نقاط نهاية باستخدام نموذج التقسيم

PYTHON
from fastapi import FastAPI, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession

app = FastAPI()

@app.get("/products", response_model=list[ProductResponse])
async def list_products(
    category: Optional[str] = Query(None),
    min_price: Optional[float] = Query(None, ge=0),
    max_price: Optional[float] = Query(None, ge=0),
    skip: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
):
    repo = ProductRepository(db)
    return await repo.get_all(
        category=category,
        min_price=min_price,
        max_price=max_price,
        skip=skip,
        limit=limit,
    )

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) مقارنة استراتيجيات التقسيم

الاستراتيجية التنفيذ المزايا العيوب
تقسيم الإزاحة OFFSET n LIMIT m بسيط، يدعم القفز بين الصفحات أداء ضعيف مع الإزاحات الكبيرة
تقسيم المؤشر WHERE id > cursor LIMIT m أداء جيد مع مجموعات البيانات الكبيرة لا يدعم القفز بين الصفحات
تقسيم Keyset WHERE created_at < last LIMIT m التقسيم حسب حقل الفرز يعمل بشكل جيد يتطلب حقلًا مرتبًا

5. التحميل المسبق للبيانات المرتبطة

(1) مشكلة استعلام N+1

(1) ▶ مثال: عرض مشكلة N+1

PYTHON
# سيء: استعلامات N+1 - استعلام واحد للمنتجات + N استعلام للأسعار
stmt = select(Product)
result = await db.execute(stmt)
products = result.scalars().all()
for p in products:
    # كل وصول يؤدي إلى استعلام منفصل!
    print(len(p.prices))  # N استعلام

الناتج:

TEXT
# التنفيذ ناجح

(2) ▶ مثال: التحميل المسبق باستخدام selectinload

PYTHON
from sqlalchemy.orm import selectinload, joinedload

# جيد: استعلامان فقط - منتجات + أسعار (باستخدام SELECT IN)
stmt = select(Product).options(selectinload(Product.prices))
result = await db.execute(stmt)
products = result.scalars().all()
for p in products:
    print(len(p.prices))  # لا استعلامات إضافية

الناتج:

TEXT
# التنفيذ ناجح

(2) selectinload مقابل joinedload

البعد selectinload joinedload
استراتيجية SQL SELECT ... WHERE id IN (...) LEFT OUTER JOIN
عدد الاستعلامات 2 استعلام 1 استعلام
حجم البيانات دقيق (بدون صفوف مكررة) JOIN قد ينتج صفوفًا مكررة
حالات الاستخدام واحد إلى متعدد (مجموعة) متعدد إلى واحد / واحد إلى واحد
الأداء أفضل لمجموعات البيانات الكبيرة أفضل لمجموعات البيانات ذات العلاقات القليلة
إزالة التكرار غير مطلوب مطلوب unique()

(3) ▶ مثال: استعلام المنتج مع تحميل الأسعار مسبقًا

PYTHON
async def get_product_with_prices(
    self, product_id: int
) -> Product | None:
    stmt = (
        select(Product)
        .options(selectinload(Product.prices))
        .where(Product.id == product_id)
    )
    result = await self.db.execute(stmt)
    return result.scalar_one_or_none()

الناتج:

TEXT
# تم تعريف الدالة بنجاح

6. إدارة المعاملات

(1) المعاملات عبر الجداول

(1) ▶ مثال: معاملة استيراد أسعار مجمعة

PYTHON
from sqlalchemy.ext.asyncio import AsyncSession

class PriceService:
    def __init__(self, db: AsyncSession):
        self.db = db
        self.price_repo = PriceRepository(db)
        self.product_repo = ProductRepository(db)

    async def bulk_import_prices(
        self,
        product_id: int,
        prices: list[PriceCreate],
    ) -> list[Price]:
        # التحقق من وجود المنتج
        product = await self.product_repo.get_by_id(product_id)
        if not product:
            raise HTTPException(status_code=404, detail="Product not found")

        # التحقق من أن جميع الأسعار تشير إلى نفس المنتج
        for p in prices:
            p.product_id = product_id

        # إدراج مجمّع داخل المعاملة (جلسة db تتعامل مع الالتزام/الإرجاع)
        db_prices = await self.price_repo.bulk_create(prices)

        # تحديث السعر الأساسي للمنتج إلى الأحدث
        latest_price = db_prices[-1]
        await self.product_repo.update(
            product_id,
            {"base_price": latest_price.price},
        )

        return db_prices

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: استيراد مجمّع لنقاط النهاية

PYTHON
@app.post("/products/{product_id}/prices/bulk", response_model=list[PriceResponse])
async def bulk_import(
    product_id: int = Path(gt=0),
    prices: list[PriceCreate] = ...,
    db: AsyncSession = Depends(get_db),
    user: dict = Depends(get_current_user),
):
    # فحص الاشتراك: المستخدمون المجانيون محدودون بـ 1000 سعر
    if user["subscription"] == "free" and len(prices) > 1000:
        raise HTTPException(
            status_code=403,
            detail="Free plan limited to 1000 prices per import. Upgrade to Pro.",
        )
    
    service = PriceService(db)
    return await service.bulk_import_prices(product_id, prices)

الناتج:

TEXT
# تم تعريف الدالة بنجاح

❓ أسئلة شائعة

س هل يجب أن يكون Repository فئة؟
ج لا، لا يجب أن يكون؛ يمكنك أيضًا استخدام دالة. ومع ذلك، الفئة تغلف حالة جلسة DB، مما يجعل مشاركة الجلسة عبر الدوال أسهل، لذا يُوصى بنهج الفئة.
س ما الفرق بين flush و commit؟
ج flush يرسل SQL إلى قاعدة البيانات ولكنه لا يلتزم المعاملة؛ يمكنه استرداد معرفات الزيادة التلقائية وتشغيل فحوصات قيود قاعدة البيانات. commit يلتزم المعاملة، مما يجعل التغييرات دائمة. في اعتمادات yield، نوصي باستخدام flush متبوعًا بـ commit تلقائي.
س هل تحدث مشكلة N+1 فقط داخل الحلقات؟
ج تحدث بشكل رئيسي عند التكرار على الخصائص المرتبطة. ومع ذلك، يمكن أيضًا تشغيلها عن طريق تسلسل Pydantic—إذا وصل response_model إلى الحقول المرتبطة. تأكد من التحميل المسبق للبيانات.
س هل يوجد حد لحجم bulk_create؟
ج لا يوجد حد مفروض من الإطار، لكن PostgreSQL يحد من عدد المعلمات المرتبطة لكل عملية إلى 32,767. الإدراج بآلاف ليس مشكلة، لكن لعشرات الآلاف نوصي بتقسيمها إلى دفعات.
س ما مدى بطء تقسيم الإزاحة مع مجموعات البيانات الكبيرة؟
ج OFFSET 1000000 يحتاج أولًا إلى مسح أول مليون صف قبل تخطيها؛ وقت الاستعلام يتناسب مع الإزاحة. لمجموعات البيانات بالملايين، نوصي باستخدام تقسيم المؤشر.
س كيف أتعامل مع الإخفاقات الجزئية داخل معاملة؟
ج يتم تشغيل الإرجاع المرتبط بـ yield تلقائيًا عند حدوث استثناء. إذا كنت بحاجة إلى إرجاع جزئي، استخدم نقطة حفظ (await session.begin_nested()).

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة ⭐): نفذ دالتي get_by_id و get_all في ProductRepository، وحقن جلسة DB في نقاط النهاية، واستدعهما. تلميح: select(Product).where(Product.id == product_id)
  2. تمرين متقدم (الصعوبة ⭐⭐): أضف دعمًا للاستعلامات المقسمة (باستخدام معلمات skip و limit)، ونفذ التحميل المسبق selectinload(Product.prices)، وأعد تفاصيل المنتج التي تتضمن الأسعار. تلميح: options(selectinload(...))
  3. تحدي (الصعوبة ⭐⭐⭐): نفذ PriceService.bulk_import_prices، وتحقق من وجود المنتج، وأدرج الأسعار بشكل مجمّع، وحدّث base_price للمنتج—كل ذلك ضمن معاملة واحدة. المستخدمون المجانيون محدودون بـ 1000 سجل. تلميح: bulk_create + update + Depends(get_current_user)

---|

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%