Asosiy kontentga o‘tish

Maqolalar 8 daqiqa 445 ko‘rish

SQLModel: bitta klass — ham jadval, ham sxema

FastAPI loyihasida odatda ikki xil klass yoziladi: bazaga SQLAlchemy modeli, so'rov-javobga Pydantic sxemasi. SQLModel shu ikkalasini bitta klassga jamlaydi. Doctolib loyihasidagi misollar bilan noldan boshlab ko'ramiz.

fastapi database python

Mundarija

FastAPI o'rganishni boshlagan odam bir joyga kelib to'xtaydi: bazadagi jadval uchun SQLAlchemy modeli yozdi, endi API'ga chiqarish uchun yana o'shaning Pydantic nusxasini yozishi kerak. Bir xil maydonlar ikki joyda, biri o'zgarsa ikkinchisini unutish oson. Men buni Doctolib loyihamda yaqqol his qilganman — shifokor, bemor, qabul degan tushunchalarning har biri ikki nusxada yashay boshlagan edi.

SQLModel aynan shu og'riq uchun yaratilgan (muallifi ham FastAPI'ning o'zini yozgan Sebastián Ramírez). G'oyasi sodda: bitta klass yozasiz, u ham baza jadvali, ham Pydantic sxemasi bo'lib xizmat qiladi.

O'rnatish va birinchi model#

Terminal
uv add sqlmodel "fastapi[standard]"

[standard] qo'shimchasi fastapi dev buyrug'ini va uvicorn serverini olib keladi — faqat uv add sqlmodel bilan FastAPI'ning o'zi ham o'rnatilmaydi.

Loyihamiz kichik shifokor qabuli tizimi bo'ladi — fayl tuzilishi shunday:

Fayl tuzilishi
app/
├── __init__.py
├── models.py       ← SQLModel klasslar
├── database.py     ← engine va session
└── main.py         ← FastAPI endpoint'lar
Python
# app/models.py
from datetime import datetime

from sqlmodel import Field, SQLModel


class DoctorBase(SQLModel):
    # umumiy maydonlar — bitta joyda
    name: str
    specialty: str = Field(index=True)  # mutaxassislik bo'yicha qidiramiz


class Doctor(DoctorBase, table=True):
    # table=True — bu klass bazada jadval bo'ladi degani; id shu yerda qo'shiladi
    id: int | None = Field(default=None, primary_key=True)


class DoctorCreate(DoctorBase):
    # table=True'siz — sof sxema: API'ga kiradigan ma'lumot, id'siz
    pass


class Appointment(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    doctor_id: int = Field(foreign_key="doctor.id")
    patient_name: str
    starts_at: datetime

E'tibor bering, bu oddiy Python klasslar — type hint'lar bilan yozilgan. id: int | None bo'lishining sababi ham tushunarli: yangi yozuv yaratilayotganda id hali yo'q, uni baza beradi. Doctor jadvali bilan DoctorCreate sxemasi maydonlarni DoctorBase dan meros oladi — takrorlanish yo'q. Nega jadval klassining o'zi API'ga kirish sxemasi bo'lmasligini pastda alohida ko'ramiz.

Engine va session#

Python
# app/database.py
from sqlmodel import Session, SQLModel, create_engine

# boshlanish uchun SQLite yetarli; keyin PostgreSQL'ga faqat shu satr o'zgaradi
engine = create_engine("sqlite:///clinic.db", echo=True)  # echo: SQL'ni ko'rsatib turadi


def init_db() -> None:
    # hamma table=True klasslar bo'yicha jadvallarni yaratadi
    SQLModel.metadata.create_all(engine)


def get_session():
    # FastAPI dependency: har so'rovga alohida session, ish tugagach yopiladi
    with Session(engine) as session:
        yield session

echo=Trueni o'rganish paytida yoqib qo'ying: har amalda qanday SQL ketayotganini ko'rib borasiz va ORM sehr emas, shunchaki tarjimon ekanini his qilasiz.

Endpoint'lar: yozish va o'qish#

Python
# app/main.py
from contextlib import asynccontextmanager

from fastapi import Depends, FastAPI, HTTPException
from sqlmodel import Session, select

from app.database import get_session, init_db
from app.models import Doctor, DoctorCreate


@asynccontextmanager
async def lifespan(app: FastAPI):
    init_db()  # ilova ishga tushayotganda jadvallarni yaratadi
    yield


app = FastAPI(lifespan=lifespan)


@app.post("/doctors/", response_model=Doctor)
def create_doctor(data: DoctorCreate, session: Session = Depends(get_session)):
    # kirish — DoctorCreate: Pydantic tekshiradi, id qabul qilinmaydi.
    # model_validate uni jadval obyektiga aylantiradi
    doctor = Doctor.model_validate(data)
    session.add(doctor)
    session.commit()
    session.refresh(doctor)  # bazadan id qaytib keladi
    return doctor


@app.get("/doctors/", response_model=list[Doctor])
def list_doctors(specialty: str | None = None, session: Session = Depends(get_session)):
    query = select(Doctor)
    if specialty:
        query = query.where(Doctor.specialty == specialty)
    return session.exec(query).all()


@app.get("/doctors/{doctor_id}", response_model=Doctor)
def get_doctor(doctor_id: int, session: Session = Depends(get_session)):
    doctor = session.get(Doctor, doctor_id)
    if doctor is None:
        raise HTTPException(404, "Bunday shifokor yo'q")
    return doctor

Ishga tushirib sinaymiz:

Terminal
uv run fastapi dev app/main.py

curl -X POST http://127.0.0.1:8000/doctors/ \
  -H "Content-Type: application/json" \
  -d '{"name": "Aziza Karimova", "specialty": "kardiolog"}'
# {"id": 1, "name": "Aziza Karimova", "specialty": "kardiolog"}

curl "http://127.0.0.1:8000/doctors/?specialty=kardiolog"

Bitta model oilasi bilan validatsiya ham, saqlash ham, hujjat ham (FastAPI'ning /docs sahifasini oching — sxema o'sha yerda tayyor turibdi) ishladi. lifespan — ilova ishga tushish va to'xtash paytidagi ishlar uchun hozirgi usul; eski @app.on_event("startup") ham ishlaydi, lekin FastAPI uni eskirgan deb ogohlantiradi.

Nega kirishga jadval klassining o'zini qo'ymaymiz#

Birinchi xayol — create_doctor(doctor: Doctor, ...) deb yozib, bitta klassni ham kirish, ham jadval qilish. Sinab ko'rdim, ikki muammo chiqdi:

  • validatsiya ishlamaydi. SQLModel table=True klassni yaratishda maydonlarni tekshirmaydi. name siz yuborilgan so'rov 422 bilan rad etilmadi — bazaga yetib borib, NOT NULL constraint failed: doctor.name bilan yiqildi va mijoz 500 oldi;

  • mijoz id ni o'zi tanlaydi. {"id": 500, ...} yuborilsa, shifokor aynan 500-raqam bilan yaratildi, mavjud id yuborilsa esa yana 500 xato.

DoctorCreate bilan ikkalasi yopiladi: name siz so'rov 422 va aniq xato matnini oladi, id esa kirish sxemasida umuman yo'q — uni faqat baza beradi. Doctor.model_validate(data) shu tekshirilgan ma'lumotdan jadval obyektini yasaydi. Javob uchun esa response_model=Doctor bemalol qoladi: chiqishda id kerak.

SQLModel ostida tanish narsalar turadi

SQLModel aslida butunlay yangi ORM emas. Uning ichida ikkita tanish kutubxona ishlaydi: baza bilan gaplashadigan tomonda SQLAlchemy, ma'lumotni tekshiradigan tomonda esa Pydantic. Buning amaliy foydasi shundaki, SQLAlchemy haqida bilganingiz bu yerda ham ishga yaraydi. Agar murakkabroq narsa kerak bo'lsa — masalan bir necha jadvalni birlashtiradigan og'ir so'rov — uni to'g'ridan-to'g'ri SQLAlchemy'ning o'zida yozaverasiz, SQLModel bunga to'sqinlik qilmaydi.

Bitta savol ochiq qoldi#

Loyihani ishga tushirdik, jadvallar create_all bilan yaratildi. Lekin ertaga Doctorga yangi maydon qo'shsangiz nima bo'ladi? create_all mavjud jadvalni o'zgartirmaydi — u faqat yo'qni yaratadi. Ishlab turgan bazani buzmasdan o'zgartirish alohida hunar va uning tool'i ham tayyor: Alembic migratsiyalari. Keyingi qismda aynan shuni ko'ramiz.

8 daqiqa 445 marta o‘qildi