404 Not Found

404 Not Found


nginx

تكامل قاعدة البيانات — SQLAlchemy غير المتزامن + Alembic

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

1. ما ستتعلمه


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

(1) نقطة الألم: تزامن قاعدة البيانات يبطئ واجهات API غير المتزامنة

تستخدم Alice SQLAlchemy المتزامن للتفاعل مع قاعدة البيانات، وكل استعلام يحظر حلقة الأحداث لمدة 50-100 مللي ثانية. عندما وصل عدد الطلبات المتزامنة إلى PriceTracker إلى 500، تم تعويض فوائد FastAPI غير المتزامنة بالكامل بسبب عمليات قاعدة البيانات المتزامنة، وقفز زمن استجابة P99 من 50 مللي ثانية المتوقعة إلى 2000 مللي ثانية. لاحظ Charlie أن تجمع اتصالات قاعدة البيانات قد استنفد، وأن الطلبات الجديدة كانت في قائمة الانتظار.

(2) حل SQLAlchemy غير المتزامن

يوفر SQLAlchemy 2.0 دعمًا غير متزامن أصلي: create_async_engine + AsyncSession. استعلامات قاعدة البيانات لم تعد تحظر حلقة الأحداث، مما يسمح لـ FastAPI بالاستفادة الكاملة من مزاياها غير المتزامنة.

PYTHON
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker

engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
async_session = async_sessionmaker(engine, expire_on_commit=False)

async def get_db():
    async with async_session() as session:
        yield session

(3) العائد

بعد الانتقال إلى SQLAlchemy غير المتزامن، انخفض زمن استجابة P99 لـ PriceTracker من 2000 مللي ثانية إلى 80 مللي ثانية، وارتفعت نسبة استخدام اتصالات قاعدة البيانات من 30% إلى 90%، وارتفع QPS للعقدة الواحدة من 500 إلى أكثر من 3000.


3. المحركات والجلسات غير المتزامنة

(1) مخطط ER لبنية قاعدة البيانات

100%
erDiagram
    users ||--o{ products : creates
    products ||--o{ prices : has
    users {
        int id PK
        string email UK
        string hashed_password
        string role
        string subscription
        datetime created_at
    }
    products {
        int id PK
        string name
        string category
        float base_price
        string description
        int user_id FK
        datetime created_at
    }
    prices {
        int id PK
        int product_id FK
        float price
        string currency
        string source
        datetime recorded_at
    }

(1) ▶ مثال: المحرك غير المتزامن وتكوين تجمع الاتصالات

PYTHON
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker

DATABASE_URL = "postgresql+asyncpg://pricetracker:secret@localhost:5432/pricetracker"

engine = create_async_engine(
    DATABASE_URL,
    echo=False,          # تعيين True لتسجيل SQL في بيئة التطوير
    pool_size=20,        # اتصالات دائمة
    max_overflow=10,     # اتصالات إضافية عند استنفاد التجمع
    pool_timeout=30,     # وقت الانتظار للحصول على اتصال متاح
    pool_recycle=3600,   # إعادة تدوير الاتصالات بعد ساعة واحدة
)

async_session = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,  # الوصول إلى الكائنات بعد الالتزام
)

الناتج:

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

(2) SQLAlchemy المتزامن مقابل غير المتزامن: مقارنة

البعد SQLAlchemy المتزامن SQLAlchemy 2.0 غير المتزامن
المحرك create_engine create_async_engine
الجلسة Session AsyncSession
البحث session.execute(stmt) await session.execute(stmt)
الإرسال session.commit() await session.commit()
المشغل psycopg2 asyncpg
الحظر نعم لا
تجمع الاتصالات QueuePool AsyncAdaptedQueuePool

4. تعريف نموذج ORM (نمط 2.0 الجديد)

(1) DeclarativeBase + Mapped + mapped_column

يستبدل SQLAlchemy 2.0 تعريف Column() القديم بـ Mapped[type] و mapped_column()، موحدًا تلميحات النوع مع تعيينات ORM.

(1) ▶ مثال: نموذج جداول PriceTracker الثلاثة

PYTHON
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Float, Integer, DateTime, ForeignKey, Index
from sqlalchemy.orm import relationship
from datetime import datetime

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
    hashed_password: Mapped[str] = mapped_column(String(255))
    role: Mapped[str] = mapped_column(String(50), default="user")
    subscription: Mapped[str] = mapped_column(String(50), default="free")
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    products: Mapped[list["Product"]] = relationship(back_populates="owner")

class Product(Base):
    __tablename__ = "products"
    __table_args__ = (
        Index("ix_products_category", "category"),
        Index("ix_products_name", "name"),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(200), nullable=False)
    category: Mapped[str] = mapped_column(String(100), nullable=False)
    base_price: Mapped[float] = mapped_column(Float, nullable=False)
    description: Mapped[str | None] = mapped_column(String(2000), nullable=True)
    user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    owner: Mapped["User"] = relationship(back_populates="products")
    prices: Mapped[list["Price"]] = relationship(back_populates="product")

class Price(Base):
    __tablename__ = "prices"
    __table_args__ = (
        Index("ix_prices_product_id", "product_id"),
        Index("ix_prices_recorded_at", "recorded_at"),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    product_id: Mapped[int] = mapped_column(Integer, ForeignKey("products.id"))
    price: Mapped[float] = mapped_column(Float, nullable=False)
    currency: Mapped[str] = mapped_column(String(3), default="USD")
    source: Mapped[str] = mapped_column(String(100))
    recorded_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    product: Mapped["Product"] = relationship(back_populates="prices")

الناتج:

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

(2) النمط القديم V1 مقابل النمط الجديد V2

البعد النمط القديم V1 النمط الجديد V2
Base declarative_base() class Base(DeclarativeBase)
تعريف الحقل Column(Integer, primary_key=True) Mapped[int] = mapped_column(...)
تلميح النوع بدون Mapped[type] نوع كامل
الحقول الاختيارية Column(String, nullable=True) Mapped[str | None]
العلاقة relationship() Mapped[list["X"]] = relationship()

5. إدارة الجلسات غير المتزامنة وحقن الاعتماد

(1) نمط اعتماد "yield"

(1) ▶ مثال: اعتماد جلسة قاعدة البيانات

PYTHON
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

الناتج:

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

(2) ▶ مثال: استخدام الجلسات غير المتزامنة في نقاط النهاية

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

app = FastAPI()

@app.get("/products/{product_id}")
async def get_product(
    product_id: int,
    db: AsyncSession = Depends(get_db),
):
    stmt = select(Product).where(Product.id == product_id)
    result = await db.execute(stmt)
    product = result.scalar_one_or_none()
    if not product:
        from fastapi import HTTPException
        raise HTTPException(status_code=404, detail="Product not found")
    return {
        "id": product.id,
        "name": product.name,
        "base_price": product.base_price,
    }

الناتج:

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

6. ترحيلات Alembic غير المتزامنة

(1) سير عمل الترحيل

100%
flowchart LR
    A[alembic revision --autogenerate -m desc] --> B[تحرير ملف الترحيل]
    B --> C[alembic upgrade head]
    C --> D[التطبيق على قاعدة البيانات]
    D --> E{هل تحتاج إرجاع؟}
    E -->|نعم| F[alembic downgrade -1]
    E -->|لا| G[متابعة التطوير]

(1) ▶ مثال: تهيئة Alembic

BASH
# تثبيت Alembic
uv add alembic

# تهيئة Alembic بقالب غير متزامن
cd pricetracker
alembic init -t async alembic

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

(2) ▶ مثال: تكوين الوضع غير المتزامن في alembic/env.py

PYTHON
# alembic/env.py (الأقسام الرئيسية)
from sqlalchemy.ext.asyncio import create_async_engine
from app.models import Base  # استيراد النماذج الخاصة بك
from app.core.config import settings

target_metadata = Base.metadata

def run_migrations_online():
    connectable = create_async_engine(settings.database_url)

    async def do_run_migrations(connection):
        context = MigrationContext.configure(
            connection=connection,
            target_metadata=target_metadata,
        )
        with context.begin_transaction():
            context.run_migrations()

    with connectable.connect() as connection:
        asyncio.run(do_run_migrations(connection))

الناتج:

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

(3) ▶ مثال: إنشاء وتطبيق الترحيل

BASH
# إنشاء ترحيل تلقائيًا من تغييرات النموذج
alembic revision --autogenerate -m "add users products prices tables"

# تطبيق الترحيل
alembic upgrade head

# إرجاع خطوة واحدة
alembic downgrade -1

# التحقق من الإصدار الحالي
alembic current

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

❓ أسئلة شائعة

س ما الفرق بين asyncpg و psycopg؟
ج asyncpg هو مشغل PostgreSQL غير متزامن بالكامل ويقدم أداءً أفضل؛ psycopg3 يدعم العمليات غير المتزامنة ولكنه مبني على مكتبة C. يُوصى باستخدام asyncpg في الإنتاج (postgresql+asyncpg://).
س ماذا يعني expire_on_commit=False؟
ج بشكل افتراضي، تنتهي صلاحية خصائص كائن ORM بعد الالتزام (والوصول إليها مرة أخرى يؤدي إلى استعلام). تعيين هذا إلى False يبقي الخصائص قابلة للوصول، مما يمنع مشاكل التحميل الكسول.
س هل تكتشف ميزة autogenerate في Alembic جميع التغييرات؟
ج لا. تكتشف إضافة أو إزالة الجداول والأعمدة، بالإضافة إلى التغييرات في الفهارس. لا تكتشف إعادة تسمية الأعمدة، والتغييرات في دلالات القيود، وما إلى ذلك؛ تتطلب هذه تعديل ملفات الترحيل يدويًا.
س كيف يجب تعيين حجم تجمع الاتصالات؟
ج الصيغة: pool_size = (أنوية المعالج * 2) + عدد الأقراص النشطة. يستخدم PriceTracker pool_size=20 + max_overflow=10 للتعامل مع ملايين الاستعلامات.
س هل هناك فرق بين Mapped[str | None] و Mapped[Optional[str]]؟
ج هما متكافئان وظيفيًا. str | None هي صيغة Python 3.10+، بينما Optional[str] هي التدوين المتوافق مع typing. يُوصى بالأولى.
س هل يمكن استخدام كود SQLAlchemy المتزامن في سياق غير متزامن؟
ج لا يمكنك استدعاء دوال SQLAlchemy المتزامنة مباشرة داخل دالة غير متزامنة. غلفها باستخدام run_in_executor أو استخدم واجهة API غير المتزامنة حصريًا.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة ⭐): قم بتكوين create_async_engine للاتصال بـ PostgreSQL، وأنشئ async_sessionmaker، واكتب اعتماد get_db باستخدام yield. تلميح: create_async_engine(DATABASE_URL, pool_size=20)
  2. تمرين متقدم (الصعوبة ⭐⭐): عرّف نماذج Product و Price لـ PriceTracker (باستخدام نمط DeclarativeBase + Mapped)، بما في ذلك علاقات المفاتيح الخارجية والفهارس. تلميح: ForeignKey("products.id") + relationship()
  3. تحدي (الصعوبة ⭐⭐⭐): قم بتهيئة Alembic في الوضع غير المتزامن، وقم بتكوين env.py، واستخدم autogenerate لإنشاء ملفات ترحيل لثلاثة جداول، ثم طبقها على قاعدة البيانات باستخدام upgrade head. تلميح: alembic init -t async alembic + تعديل target_metadata في env.py

---|

Web-Tutorial.com

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

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

100%