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.
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 "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:
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 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#
# 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 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:
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=Trueklassni yaratishda maydonlarni tekshirmaydi.namesiz yuborilgan so'rov422bilan rad etilmadi — bazaga yetib borib,NOT NULL constraint failed: doctor.namebilan yiqildi va mijoz500oldi; -
mijoz
idni o'zi tanlaydi.{"id": 500, ...}yuborilsa, shifokor aynan 500-raqam bilan yaratildi, mavjudidyuborilsa esa yana500xato.
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.