Asosiy kontentga o‘tish

Maqolalar

Darslik · 2026-08-16 · ~7 daqiqa o‘qiladi · boshlang'ich

SQLModel: bitta klass — ham jadval, ham sxema

#python #fastapi #database

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#

uv add sqlmodel

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

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

from sqlmodel import Field, SQLModel


class Doctor(SQLModel, table=True):
    # table=True — bu klass bazada jadval bo'ladi degani.
    # Usiz u faqat sxema bo'lib qoladi (buni keyinroq ishlatamiz)
    id: int | None = Field(default=None, primary_key=True)
    name: str
    specialty: str = Field(index=True)  # mutaxassislik bo'yicha qidiramiz


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.

Engine va session#

# 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#

# app/main.py
from fastapi import Depends, FastAPI, HTTPException
from sqlmodel import Session, select

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

app = FastAPI()


@app.on_event("startup")
def on_startup():
    init_db()


@app.post("/doctors/", response_model=Doctor)
def create_doctor(doctor: Doctor, session: Session = Depends(get_session)):
    # bitta klass ikki rolda: kirishda Pydantic tekshiruvi,
    # session.add'da esa baza qatori
    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:

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 klass bilan validatsiya ham, saqlash ham, hujjat ham (FastAPI'ning /docs sahifasini oching — sxema o'sha yerda tayyor turibdi) ishladi.

Kirish va chiqishni ajratish kerak bo'lganda#

Loyiha o'sgani sari "hamma maydonni hammaga ko'rsatmaslik" ehtiyoji chiqadi. Masalan, yaratishda mijoz id yubormasligi kerak, javobda esa ichki maydonlar ko'rinmasligi mumkin. SQLModel'da bu meros bilan chiroyli yechiladi — table=Truesiz klasslar sof sxema bo'lib xizmat qiladi:

# app/models.py (davomi)
class DoctorBase(SQLModel):
    name: str
    specialty: str


class Doctor(DoctorBase, table=True):  # jadval: id qo'shiladi
    id: int | None = Field(default=None, primary_key=True)


class DoctorCreate(DoctorBase):  # kirish sxemasi: id yo'q
    pass

Endpoint'da esa def create_doctor(data: DoctorCreate, ...) deb yozasiz va Doctor.model_validate(data) bilan jadval obyektiga aylantirasiz. Umumiy maydonlar baribir bitta joyda — DoctorBaseda — turadi, takrorlanish yo'q.

SQLModel ostida tanish narsalar yotibdi

SQLModel — yangi ORM emas: ostida SQLAlchemy'ning o'zi, sxema tomonida Pydantic'ning o'zi. Demak SQLAlchemy hujjatidagi bilim shu yerda ham ishlaydi va murakkab holatda (masalan qo'shma so'rovlar) o'sha darajaga bemalol tushib olsangiz bo'ladi.

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 asbobi ham tayyor: Alembic migratsiyalari. Keyingi qismda aynan shuni ko'ramiz.