با تسلط بر ادغام ناهمزمان پایگاه داده در FastAPI، برنامه های وب با کارایی بالا را باز کنید. یک راهنمای جامع با مثال هایی از SQLAlchemy و کتابخانه Databases.
ادغام پایگاه داده FastAPI: بررسی عمیق عملیات ناهمزمان پایگاه داده
در دنیای توسعه وب مدرن، عملکرد فقط یک ویژگی نیست؛ بلکه یک الزام اساسی است. کاربران انتظار برنامه های سریع و پاسخگو را دارند و توسعه دهندگان دائماً به دنبال ابزارها و تکنیک هایی برای برآوردن این انتظارات هستند. FastAPI به عنوان یک نیروگاه در اکوسیستم پایتون ظاهر شده است و به دلیل سرعت باورنکردنی خود مورد تجلیل قرار می گیرد، که تا حد زیادی به دلیل ماهیت ناهمزمان آن است. با این حال، یک فریم ورک سریع تنها یک بخش از معادله است. اگر برنامه شما بیشتر وقت خود را صرف انتظار برای یک پایگاه داده کند می کند، شما یک موتور با کارایی بالا ایجاد کرده اید که در ترافیک گیر کرده است.
اینجاست که عملیات ناهمزمان پایگاه داده حیاتی می شود. با اجازه دادن به برنامه FastAPI خود برای رسیدگی به پرس و جوهای پایگاه داده بدون مسدود کردن کل فرآیند، می توانید همزمانی واقعی را باز کنید و برنامه هایی بسازید که نه تنها سریع هستند، بلکه بسیار مقیاس پذیر نیز هستند. این راهنمای جامع شما را از طریق چرایی، چیستی و چگونگی ادغام پایگاه داده های ناهمزمان با FastAPI راهنمایی می کند و به شما این امکان را می دهد که خدمات با کارایی بالا را برای مخاطبان جهانی بسازید.
مفهوم اصلی: چرا ورودی/خروجی ناهمزمان مهم است
قبل از اینکه وارد کد شویم، درک مشکل اساسی که عملیات ناهمزمان حل می کنند بسیار مهم است: انتظار محدود به ورودی/خروجی.
یک سرآشپز ماهر را در آشپزخانه تصور کنید. در یک مدل همزمان (یا مسدود کننده)، این سرآشپز یک کار را در یک زمان انجام می دهد. آنها یک قابلمه آب را روی اجاق می گذارند تا بجوشد و سپس همانجا می ایستند و آن را تماشا می کنند تا بجوشد. فقط پس از جوش آمدن آب است که به خرد کردن سبزیجات می پردازند. این فوق العاده ناکارآمد است. وقت سرآشپز (CPU) در طول دوره انتظار (عملیات ورودی/خروجی) تلف می شود.
اکنون، یک مدل ناهمزمان (غیر مسدود کننده) را در نظر بگیرید. سرآشپز آب را می گذارد تا بجوشد و به جای انتظار، بلافاصله شروع به خرد کردن سبزیجات می کند. آنها همچنین ممکن است یک سینی را در فر قرار دهند. آنها می توانند بین کارها جابجا شوند و در چندین جبهه پیشرفت کنند در حالی که منتظر عملیات کندتر (مانند جوشاندن آب یا پخت) برای تکمیل هستند. وقتی یک کار تمام شد (آب می جوشد)، به سرآشپز اطلاع داده می شود و می تواند با مرحله بعدی آن غذا ادامه دهد.
در یک برنامه وب، پرس و جوهای پایگاه داده، فراخوانی های API و خواندن فایل ها معادل انتظار برای جوشیدن آب هستند. یک برنامه همزمان سنتی یک درخواست را انجام می دهد، یک پرس و جو به پایگاه داده ارسال می کند و سپس بیکار می نشیند و هر درخواست ورودی دیگری را تا زمانی که پایگاه داده پاسخ دهد، مسدود می کند. یک برنامه ناهمزمان، که توسط `asyncio` پایتون و فریم ورک هایی مانند FastAPI پشتیبانی می شود، می تواند هزاران اتصال همزمان را با جابجایی کارآمد بین آنها هر زمان که یکی منتظر ورودی/خروجی است، مدیریت کند.
مزایای کلیدی عملیات ناهمزمان پایگاه داده:
- افزایش همزمانی: تعداد قابل توجهی بیشتر از کاربران همزمان را با همان منابع سخت افزاری مدیریت کنید.
- بهبود توان عملیاتی: درخواست های بیشتری را در ثانیه پردازش کنید، زیرا برنامه در انتظار پایگاه داده گیر نمی کند.
- تجربه کاربری بهبود یافته: زمان پاسخگویی سریعتر منجر به تجربه ای پاسخگوتر و رضایت بخش تر برای کاربر نهایی می شود.
- بهره وری منابع: استفاده بهتر از CPU و حافظه، که می تواند منجر به کاهش هزینه های زیرساخت شود.
راه اندازی محیط توسعه ناهمزمان خود
برای شروع، به چند جزء کلیدی نیاز دارید. ما از PostgreSQL به عنوان پایگاه داده خود برای این مثال ها استفاده خواهیم کرد زیرا از درایورهای ناهمزمان پشتیبانی بسیار خوبی دارد. با این حال، اصول برای سایر پایگاه داده ها مانند MySQL و SQLite که دارای درایورهای ناهمزمان هستند نیز اعمال می شود.
1. فریم ورک و سرور اصلی
ابتدا FastAPI و یک سرور ASGI مانند Uvicorn را نصب کنید.
pip install fastapi uvicorn[standard]
2. انتخاب ابزار ناهمزمان پایگاه داده شما
برای صحبت با پایگاه داده خود به صورت ناهمزمان به دو جزء اصلی نیاز دارید:
- یک درایور ناهمزمان پایگاه داده: این کتابخانه سطح پایین است که با استفاده از یک پروتکل ناهمزمان از طریق شبکه با پایگاه داده ارتباط برقرار می کند. برای PostgreSQL،
asyncpgاستاندارد de facto است و به دلیل عملکرد باورنکردنی خود شناخته شده است. - یک سازنده پرس و جو ناهمزمان یا ORM: این یک روش سطح بالاتر و پایتونیک تر برای نوشتن پرس و جوهای شما ارائه می دهد. ما دو گزینه محبوب را بررسی خواهیم کرد:
databases: یک سازنده پرس و جو ناهمزمان ساده و سبک وزن که یک API تمیز برای اجرای SQL خام ارائه می دهد.SQLAlchemy 2.0+: آخرین نسخه های ORM قدرتمند و غنی از ویژگی SQLAlchemy شامل پشتیبانی بومی و درجه یک برای `asyncio` است. این اغلب انتخاب ترجیحی برای برنامه های پیچیده است.
3. نصب
بیایید کتابخانه های لازم را نصب کنیم. می توانید یکی از ابزارها را انتخاب کنید یا هر دو را برای آزمایش نصب کنید.
برای PostgreSQL با SQLAlchemy و `databases`:
# Driver for PostgreSQL
pip install asyncpg
# For the SQLAlchemy 2.0+ approach
pip install sqlalchemy
# For the 'databases' library approach
pip install databases[postgresql]
با آماده شدن محیط ما، بیایید بررسی کنیم که چگونه این ابزارها را در یک برنامه FastAPI ادغام کنیم.
استراتژی 1: سادگی با کتابخانه `databases`
کتابخانه databases یک نقطه شروع عالی است. این کتابخانه به گونه ای طراحی شده است که ساده باشد و یک wrapper نازک بر روی درایورهای ناهمزمان زیربنایی ارائه می دهد و به شما قدرت SQL خام ناهمزمان را بدون پیچیدگی یک ORM کامل می دهد.
مرحله 1: اتصال به پایگاه داده و مدیریت چرخه حیات
در یک برنامه واقعی، نمی خواهید در هر درخواست به پایگاه داده متصل و قطع شوید. این ناکارآمد است. در عوض، ما یک استخر اتصال را هنگام شروع برنامه ایجاد می کنیم و به طور مرتب هنگام خاموش شدن آن را می بندیم. مدیریت کننده های رویداد FastAPI (`@app.on_event("startup")` و `@app.on_event("shutdown")`) برای این کار عالی هستند.
بیایید یک فایل به نام main_databases.py ایجاد کنیم:
import databases
import sqlalchemy
from fastapi import FastAPI
# --- Database Configuration ---
# Replace with your actual database URL
# Format for asyncpg: "postgresql+asyncpg://user:password@host/dbname"
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/testdb"
database = databases.Database(DATABASE_URL)
# SQLAlchemy model metadata (for table creation)
metadata = sqlalchemy.MetaData()
# Define a sample table
notes = sqlalchemy.Table(
"notes",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column("title", sqlalchemy.String(100)),
sqlalchemy.Column("content", sqlalchemy.String(500)),
)
# Create an engine for table creation (this part is synchronous)
# The 'databases' library doesn't handle schema creation
engine = sqlalchemy.create_engine(DATABASE_URL.replace("+asyncpg", ""))
metadata.create_all(engine)
# --- FastAPI Application ---
app = FastAPI(title="FastAPI with Databases Library")
@app.on_event("startup")
async def startup():
print("Connecting to database...")
await database.connect()
print("Database connection established.")
@app.on_event("shutdown")
async def shutdown():
print("Disconnecting from database...")
await database.disconnect()
print("Database connection closed.")
# --- API Endpoints ---
@app.get("/")
def read_root():
return {"message": "Welcome to the Async Database API!"}
نکات کلیدی:
- ما
DATABASE_URLرا با استفاده از طرحpostgresql+asyncpgتعریف می کنیم. - یک شی
databaseسراسری ایجاد می شود. - مدیریت کننده رویداد
startup،await database.connect()را فراخوانی می کند که استخر اتصال را مقداردهی اولیه می کند. - مدیریت کننده رویداد
shutdown،await database.disconnect()را فراخوانی می کند تا همه اتصالات را به طور مرتب ببندد.
مرحله 2: پیاده سازی نقاط پایانی CRUD ناهمزمان
اکنون، بیایید نقاط پایانی را برای انجام عملیات ایجاد، خواندن، به روز رسانی و حذف (CRUD) اضافه کنیم. ما همچنین از Pydantic برای اعتبارسنجی و سریال سازی داده ها استفاده خواهیم کرد.
موارد زیر را به فایل main_databases.py خود اضافه کنید:
from pydantic import BaseModel
from typing import List, Optional
# --- Pydantic Models for data validation ---
class NoteIn(BaseModel):
title: str
content: str
class Note(BaseModel):
id: int
title: str
content: str
# --- CRUD Endpoints ---
@app.post("/notes/", response_model=Note)
async def create_note(note: NoteIn):
"""Create a new note in the database."""
query = notes.insert().values(title=note.title, content=note.content)
last_record_id = await database.execute(query)
return {**note.dict(), "id": last_record_id}
@app.get("/notes/", response_model=List[Note])
async def read_all_notes():
"""Retrieve all notes from the database."""
query = notes.select()
return await database.fetch_all(query)
@app.get("/notes/{note_id}", response_model=Note)
async def read_note(note_id: int):
"""Retrieve a single note by its ID."""
query = notes.select().where(notes.c.id == note_id)
result = await database.fetch_one(query)
if result is None:
raise HTTPException(status_code=404, detail="Note not found")
return result
@app.put("/notes/{note_id}", response_model=Note)
async def update_note(note_id: int, note: NoteIn):
"""Update an existing note."""
query = (
notes.update()
.where(notes.c.id == note_id)
.values(title=note.title, content=note.content)
)
result = await database.execute(query)
if result == 0:
raise HTTPException(status_code=404, detail="Note not found")
return {**note.dict(), "id": note_id}
@app.delete("/notes/{note_id}")
async def delete_note(note_id: int):
"""Delete a note by its ID."""
query = notes.delete().where(notes.c.id == note_id)
result = await database.execute(query)
if result == 0:
raise HTTPException(status_code=404, detail="Note not found")
return {"message": "Note deleted successfully"}
تجزیه و تحلیل فراخوانی های ناهمزمان:
await database.execute(query): برای عملیاتی استفاده می شود که ردیفی را برنمی گردانند، مانند INSERT، UPDATE و DELETE. این تعداد ردیف های تحت تاثیر یا کلید اصلی رکورد جدید را برمی گرداند.await database.fetch_all(query): برای پرس و جوهای SELECT استفاده می شود که در آن انتظار دارید چندین ردیف وجود داشته باشد. این یک لیست از رکوردها را برمی گرداند.await database.fetch_one(query): برای پرس و جوهای SELECT استفاده می شود که در آن انتظار دارید حداکثر یک ردیف وجود داشته باشد. این یک رکورد تکی یاNoneرا برمی گرداند.
توجه داشته باشید که هر تعامل پایگاه داده با await شروع می شود. این جادویی است که به حلقه رویداد اجازه می دهد تا در حین انتظار برای پاسخ پایگاه داده، به کارهای دیگر переключиться کند و همزمانی بالایی را فعال کند.
استراتژی 2: نیروگاه مدرن - SQLAlchemy 2.0+ Async ORM
در حالی که کتابخانه databases برای سادگی عالی است، بسیاری از برنامه های کاربردی در مقیاس بزرگ از یک Object-Relational Mapper (ORM) با ویژگی های کامل بهره مند می شوند. یک ORM به شما این امکان را می دهد که با رکوردهای پایگاه داده به عنوان اشیاء پایتون کار کنید، که می تواند به طور قابل توجهی بهره وری توسعه دهنده و قابلیت نگهداری کد را بهبود بخشد. SQLAlchemy قدرتمندترین ORM در دنیای پایتون است و نسخه های 2.0+ آن یک رابط ناهمزمان بومی پیشرفته ارائه می دهند.
مرحله 1: راه اندازی موتور و جلسه ناهمزمان
هسته عملکرد ناهمزمان SQLAlchemy در AsyncEngine و AsyncSession قرار دارد. راه اندازی کمی متفاوت از نسخه همزمان است.
ما کد خود را برای ساختار بهتر در چند فایل سازماندهی خواهیم کرد: database.py، models.py، schemas.py و main_sqlalchemy.py.
database.py:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/testdb"
# Create an async engine
engine = create_async_engine(DATABASE_URL, echo=True)
# Create a session factory
# expire_on_commit=False prevents attributes from being expired after commit
AsyncSessionLocal = sessionmaker(
bind=engine, class_=AsyncSession, expire_on_commit=False
)
models.py:
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Note(Base):
__tablename__ = "notes"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(100), index=True)
content = Column(String(500))
schemas.py (مدل های Pydantic):
from pydantic import BaseModel
class NoteBase(BaseModel):
title: str
content: str
class NoteCreate(NoteBase):
pass
class Note(NoteBase):
id: int
class Config:
orm_mode = True
`orm_mode = True` در کلاس پیکربندی مدل Pydantic یک قطعه جادویی کلیدی است. این به Pydantic می گوید که داده ها را نه تنها از دیکشنری ها، بلکه از атрибуتهای مدل ORM نیز بخواند.
مرحله 2: مدیریت جلسات با تزریق وابستگی
روش توصیه شده برای مدیریت جلسات پایگاه داده در FastAPI از طریق تزریق وابستگی است. ما یک وابستگی ایجاد خواهیم کرد که یک جلسه پایگاه داده را برای یک درخواست ارائه می دهد و اطمینان می دهد که بعداً بسته می شود، حتی اگر خطایی رخ دهد.
این را به main_sqlalchemy.py خود اضافه کنید:
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from . import models, schemas
from .database import engine, AsyncSessionLocal
app = FastAPI()
# --- Dependency for getting a DB session ---
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
# --- Database Initialization (for creating tables) ---
@app.on_event("startup")
async def startup_event():
print("Initializing database schema...")
async with engine.begin() as conn:
# await conn.run_sync(models.Base.metadata.drop_all)
await conn.run_sync(models.Base.metadata.create_all)
print("Database schema initialized.")
وابستگی get_db سنگ بنای این الگو است. برای هر درخواست به نقطه پایانی که از آن استفاده می کند، این کار را انجام می دهد:
- یک
AsyncSessionجدید ایجاد کنید. yieldجلسه را به تابع نقطه پایانی بفرستید.- کد داخل بلوک
finallyاطمینان می دهد که جلسه بسته شده است و اتصال را صرف نظر از اینکه درخواست موفقیت آمیز بوده است یا خیر، به استخر برمی گرداند.
مرحله 3: پیاده سازی Async CRUD با SQLAlchemy ORM
اکنون می توانیم نقاط پایانی خود را بنویسیم. آنها تمیزتر و شی گرا تر از رویکرد SQL خام به نظر می رسند.
این نقاط پایانی را به main_sqlalchemy.py اضافه کنید:
@app.post("/notes/", response_model=schemas.Note)
async def create_note(
note: schemas.NoteCreate, db: AsyncSession = Depends(get_db)
):
db_note = models.Note(title=note.title, content=note.content)
db.add(db_note)
await db.commit()
await db.refresh(db_note)
return db_note
@app.get("/notes/", response_model=list[schemas.Note])
async def read_all_notes(skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(models.Note).offset(skip).limit(limit))
notes = result.scalars().all()
return notes
@app.get("/notes/{note_id}", response_model=schemas.Note)
async def read_note(note_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(models.Note).filter(models.Note.id == note_id))
db_note = result.scalar_one_or_none()
if db_note is None:
raise HTTPException(status_code=404, detail="Note not found")
return db_note
@app.put("/notes/{note_id}", response_model=schemas.Note)
async def update_note(
note_id: int, note: schemas.NoteCreate, db: AsyncSession = Depends(get_db)
):
result = await db.execute(select(models.Note).filter(models.Note.id == note_id))
db_note = result.scalar_one_or_none()
if db_note is None:
raise HTTPException(status_code=404, detail="Note not found")
db_note.title = note.title
db_note.content = note.content
await db.commit()
await db.refresh(db_note)
return db_note
@app.delete("/notes/{note_id}")
async def delete_note(note_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(models.Note).filter(models.Note.id == note_id))
db_note = result.scalar_one_or_none()
if db_note is None:
raise HTTPException(status_code=404, detail="Note not found")
await db.delete(db_note)
await db.commit()
return {"message": "Note deleted successfully"}
تجزیه و تحلیل الگوی ناهمزمان SQLAlchemy:
db: AsyncSession = Depends(get_db): این جلسه پایگاه داده ما را به نقطه پایانی تزریق می کند.await db.execute(...): این روش اصلی برای اجرای پرس و جوها است.result.scalars().all()/result.scalar_one_or_none(): این روش ها برای استخراج اشیاء ORM واقعی از نتیجه پرس و جو استفاده می شوند.db.add(obj): یک شی را برای درج آماده می کند.await db.commit(): تراکنش را به طور ناهمزمان به پایگاه داده متعهد می کند. این یک نقطهawaitحیاتی است.await db.refresh(obj): شی پایتون را با هر داده جدید از پایگاه داده پس از تعهد (مانند شناسه تولید شده خودکار) تازه می کند.
ملاحظات عملکرد و بهترین شیوه ها
به سادگی استفاده از `async` و `await` یک شروع عالی است، اما برای ساخت برنامه های واقعاً قوی و با کارایی بالا، این بهترین شیوه ها را در نظر بگیرید.
1. درک تجمع اتصال
هم databases و هم AsyncEngine SQLAlchemy یک استخر اتصال را در پشت صحنه مدیریت می کنند. این استخر مجموعه ای از اتصالات باز پایگاه داده را حفظ می کند که می توانند توسط درخواست های مختلف استفاده مجدد شوند. این امر از سربار پرهزینه ایجاد یک اتصال TCP جدید و احراز هویت با پایگاه داده برای هر پرس و جو جلوگیری می کند. می توانید اندازه استخر را تنظیم کنید (به عنوان مثال، `pool_size`, `max_overflow`) در پیکربندی موتور برای حجم کاری خاص خود.
2. هرگز فراخوانی های پایگاه داده همزمان و ناهمزمان را با هم ترکیب نکنید
مهمترین قانون این است که هرگز یک تابع ورودی/خروجی مسدود کننده همزمان را در داخل یک تابع `async def` فراخوانی نکنید. یک فراخوانی پایگاه داده همزمان استاندارد (به عنوان مثال، با استفاده مستقیم از `psycopg2`) کل حلقه رویداد را مسدود می کند، برنامه شما را مسدود می کند و هدف از ناهمزمان را از بین می برد.
اگر مطلقاً باید یک قطعه کد همزمان را اجرا کنید (شاید یک کتابخانه محدود به CPU)، از `run_in_threadpool` FastAPI برای جلوگیری از مسدود کردن حلقه رویداد استفاده کنید:
from fastapi.concurrency import run_in_threadpool
@app.get("/run-sync-task/")
async def run_sync_task():
# 'some_blocking_io_function' is a regular sync function
result = await run_in_threadpool(some_blocking_io_function, arg1, arg2)
return {"result": result}
3. از تراکنش های ناهمزمان استفاده کنید
هنگامی که یک عملیات شامل چندین تغییر پایگاه داده است که باید با هم موفق یا ناموفق شوند (یک عملیات اتمی)، باید از یک تراکنش استفاده کنید. هر دو کتابخانه از این طریق از طریق یک مدیر زمینه ناهمزمان پشتیبانی می کنند.
با `databases`:
async def transfer_funds():
async with database.transaction():
await database.execute(query_for_debit)
await database.execute(query_for_credit)
با SQLAlchemy:
async def transfer_funds(db: AsyncSession = Depends(get_db)):
async with db.begin(): # This starts a transaction
# Find accounts
account_from = ...
account_to = ...
# Update balances
account_from.balance -= 100
account_to.balance += 100
# The transaction is automatically committed on exiting the block
# or rolled back if an exception occurs.
4. فقط آنچه را که نیاز دارید انتخاب کنید
هنگامی که فقط به چند ستون نیاز دارید، از `SELECT *` اجتناب کنید. انتقال داده های کمتر از طریق شبکه زمان انتظار ورودی/خروجی را کاهش می دهد. با SQLAlchemy، می توانید از `options(load_only(model.col1, model.col2))` برای تعیین ستون هایی که باید بازیابی شوند استفاده کنید.
نتیجه گیری: آینده ناهمزمان را در آغوش بگیرید
ادغام عملیات ناهمزمان پایگاه داده در برنامه FastAPI شما کلید باز کردن پتانسیل کامل عملکرد آن است. با اطمینان از اینکه برنامه شما در حین انتظار برای پایگاه داده مسدود نمی شود، می توانید خدماتی بسازید که فوق العاده سریع، مقیاس پذیر و کارآمد هستند و می توانند بدون عرق ریختن به یک پایگاه کاربری جهانی خدمات ارائه دهند.
ما دو استراتژی قدرتمند را بررسی کرده ایم:
- کتابخانه `databases` یک رویکرد ساده و سبک وزن را برای توسعه دهندگانی ارائه می دهد که ترجیح می دهند SQL بنویسند و به یک رابط ناهمزمان ساده و سریع نیاز دارند.
- SQLAlchemy 2.0+ یک ORM کامل، قوی با یک API ناهمزمان بومی ارائه می دهد، که آن را به انتخاب ایده آل برای برنامه های پیچیده ای تبدیل می کند که در آن بهره وری و قابلیت نگهداری توسعه دهنده در اولویت قرار دارد.
انتخاب بین آنها به نیازهای پروژه شما بستگی دارد، اما اصل اصلی یکسان است: غیر مسدود کننده فکر کنید. با اتخاذ این الگوها و بهترین شیوه ها، شما فقط کد نمی نویسید. شما در حال طراحی سیستم هایی برای تقاضاهای همزمانی بالای وب مدرن هستید. ساخت برنامه FastAPI با کارایی بالای بعدی خود را امروز شروع کنید و قدرت پایتون ناهمزمان را از نزدیک تجربه کنید.