تكامل قاعدة البيانات — SQLAlchemy غير المتزامن + Alembic
قاعدة البيانات مثل المستودع—إذا كان عدد الأبواب (الاتصالات) قليلًا جدًا، تتراكم البضائع (الاستعلامات)؛ وإذا كان التخطيط (الفهارس) فوضويًا، فإن العثور على عنصر واحد (قطعة بيانات) يتطلب البحث في المستودع بالكامل.
1. ما ستتعلمه
- محرك SQLAlchemy 2.0 غير المتزامن:
create_async_engine،AsyncSession - تعريف نماذج ORM:
DeclarativeBase،Mapped،mapped_column(نمط 2.0 الجديد) - ترحيل Alembic غير المتزامن: تكوين
env.pyلوضعrun_migrations_onlineغير المتزامن - إدارة الجلسات غير المتزامنة: أنماط حقن الاعتماد
async with/yield - سيناريو Alice: تصميم النماذج غير المتزامنة لجداول PriceTracker الثلاثة
products/prices/users
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 بالاستفادة الكاملة من مزاياها غير المتزامنة.
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 لبنية قاعدة البيانات
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) ▶ مثال: المحرك غير المتزامن وتكوين تجمع الاتصالات
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, # الوصول إلى الكائنات بعد الالتزام
)
الناتج:
# التنفيذ ناجح
(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 الثلاثة
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")
الناتج:
# التنفيذ ناجح
(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) ▶ مثال: اعتماد جلسة قاعدة البيانات
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()
الناتج:
# تم تعريف الدالة بنجاح
(2) ▶ مثال: استخدام الجلسات غير المتزامنة في نقاط النهاية
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,
}
الناتج:
# تم تعريف الدالة بنجاح
6. ترحيلات Alembic غير المتزامنة
(1) سير عمل الترحيل
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
# تثبيت Alembic
uv add alembic
# تهيئة Alembic بقالب غير متزامن
cd pricetracker
alembic init -t async alembic
الناتج:
# تم تنفيذ الأمر بنجاح
(2) ▶ مثال: تكوين الوضع غير المتزامن في alembic/env.py
# 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))
الناتج:
# تم تعريف الدالة بنجاح
(3) ▶ مثال: إنشاء وتطبيق الترحيل
# إنشاء ترحيل تلقائيًا من تغييرات النموذج
alembic revision --autogenerate -m "add users products prices tables"
# تطبيق الترحيل
alembic upgrade head
# إرجاع خطوة واحدة
alembic downgrade -1
# التحقق من الإصدار الحالي
alembic current
الناتج:
# تم تنفيذ الأمر بنجاح
❓ أسئلة شائعة
postgresql+asyncpg://).expire_on_commit=False؟False يبقي الخصائص قابلة للوصول، مما يمنع مشاكل التحميل الكسول.autogenerate في Alembic جميع التغييرات؟Mapped[str | None] و Mapped[Optional[str]]؟str | None هي صيغة Python 3.10+، بينما Optional[str] هي التدوين المتوافق مع typing. يُوصى بالأولى.run_in_executor أو استخدم واجهة API غير المتزامنة حصريًا.📖 ملخص
- محرك SQLAlchemy 2.0 غير المتزامن (
create_async_engine+AsyncSession) يتميز بعدم حظر حلقة الأحداث والاستفادة الكاملة من مزايا FastAPI غير المتزامنة Mapped[type]+mapped_column()هو نمط 2.0 الجديد، الذي يوحد تلميحات النوع مع تعيينات ORM- نمط اعتماد
yieldيدير دورة حياة الجلسات غير المتزامنة: التزام وإرجاع وإغلاق تلقائي - تُهيأ ترحيلات Alembic غير المتزامنة باستخدام القالب
-t async، ويقوم قالبenv.pyبتكوين المحرك غير المتزامن - نموذج جداول PriceTracker الثلاثة (users/products/prices) يتضمن استراتيجيات الفهرسة ويدعم استعلامات ملايين السجلات
📝 تمارين
- تمرين أساسي (الصعوبة ⭐): قم بتكوين
create_async_engineللاتصال بـ PostgreSQL، وأنشئasync_sessionmaker، واكتب اعتمادget_dbباستخدام yield. تلميح:create_async_engine(DATABASE_URL, pool_size=20) - تمرين متقدم (الصعوبة ⭐⭐): عرّف نماذج
ProductوPriceلـ PriceTracker (باستخدام نمط DeclarativeBase + Mapped)، بما في ذلك علاقات المفاتيح الخارجية والفهارس. تلميح:ForeignKey("products.id")+relationship() - تحدي (الصعوبة ⭐⭐⭐): قم بتهيئة Alembic في الوضع غير المتزامن، وقم بتكوين
env.py، واستخدمautogenerateلإنشاء ملفات ترحيل لثلاثة جداول، ثم طبقها على قاعدة البيانات باستخدامupgrade head. تلميح:alembic init -t async alembic+ تعديلtarget_metadataفيenv.py
---|



