Asosiy kontentga o‘tish

Maqolalar 8 daqiqa 782 ko‘rish

Alembic: ishlab turgan bazani buzmasdan o'zgartirish

Modelga yangi maydon qo'shdingiz — bazada esa uni hech kim yaratmaydi. Django'da migrate bor edi, FastAPI dunyosida bu ishni Alembic qiladi. Sozlash, avtomatik migratsiya yaratish va uni ehtiyotkorlik bilan qo'llashni birma-bir ko'ramiz.

fastapi database python

Mundarija

Avvalgi qismda klinika API'sini qurdik va jadvallarni create_all bilan yaratdik. Oxirida bitta savol ochiq qolgan edi: Doctor modeliga yangi maydon qo'shsak nima bo'ladi? Sinab ko'ring — hech narsa bo'lmaydi. create_all faqat yo'q jadvalni yaratadi, borini o'zgartirmaydi. Lokalda bazani o'chirib qaytadan yaratish mumkin, lekin ichida haqiqiy yozuvlar turgan serverda bu yo'l yopiq.

Django'dan kelganlar uchun bu tanish muammo — u yerda makemigrations va migrate bor edi. FastAPI o'z ORM'iga ega bo'lmagani uchun bu ish alohida tool'ga yuklangan: Alembic — SQLAlchemy oilasining rasmiy migratsiya vositasi, SQLModel bilan ham bemalol ishlaydi.

Sozlash: bir marta qilinadigan ish#

Terminal
uv add alembic
uv run alembic init migrations

Ikkinchi buyruq loyihada yangi papka va fayl ochadi:

Fayl tuzilishi
.
├── alembic.ini          ← asosiy sozlama
├── migrations/
│   ├── env.py           ← Alembic'ning "miyasi" — shu faylni sozlaymiz
│   └── versions/        ← migratsiya fayllari shu yerga tushadi
└── app/
    ├── models.py
    └── database.py

Ikki joyga tegamiz. Avval alembic.inida baza manzilini ko'rsatamiz:

INI
# alembic.ini
sqlalchemy.url = sqlite:///clinic.db

Keyin env.pyga modellarimizni tanishtiramiz — Alembic "hozir baza qanday bo'lishi kerak"ligini bilishi uchun aynan shu kerak:

Python
# migrations/env.py — mavjud faylda uch joyni o'zgartiramiz

# 1) yuqoriga import qo'shamiz. Modellar import bo'lishi SHART —
#    aks holda Alembic ularni "ko'rmaydi"
from sqlmodel import SQLModel

from app import models  # noqa: F401

# 2) target_metadata = None qatorini almashtiramiz:
target_metadata = SQLModel.metadata

# 3) faylda ikkita context.configure(...) chaqiruvi bor — ikkalasiga ham
#    shu argumentni qo'shamiz (nega kerakligi pastda, SQLite bo'limida):
#    context.configure(..., render_as_batch=True)

Bitta mayda, lekin tishlaydigan joy: migratsiya fayllari ichida SQLModel tiplari ishlatiladi (sqlmodel.sql.sqltypes.AutoString), shuning uchun migrations/script.py.mako shabloniga bitta import qo'shib qo'ying — busiz upgrade NameError: name 'sqlmodel' is not defined bilan yiqiladi:

Python
# migrations/script.py.mako ichida, boshqa importlar yoniga:
import sqlmodel

Birinchi migratsiya#

Bu yerda eng ko'p adashiladigan joy bor. --autogenerate modellarni bazaning hozirgi holati bilan solishtiradi. Bizning baza esa 1-qismda create_all bilan allaqachon yaratilgan — jadvallar joyida. Shu bazaga qarshi yaratilgan "boshlang'ich" migratsiya bo'sh chiqadi: upgrade() ichida faqat pass. Sinovda aynan shunday bo'ldi, va keyin bo'sh bazada (masalan, yangi serverda) upgrade head faqat alembic_version jadvalini yaratdi — doctor ham, appointment ham yo'q edi.

To'g'ri yo'l: boshlang'ich migratsiyani bo'sh bazaga qarshi yaratamiz, keyin mavjud bazaga "sen allaqachon shu holatdasan" deb belgi qo'yamiz:

Terminal
mv clinic.db clinic.db.bak      # hozirgi bazani vaqtincha chetga olamiz
uv run alembic revision --autogenerate -m "boshlang'ich holat"
mv clinic.db.bak clinic.db      # asl bazani joyiga qaytaramiz

# migratsiyani bajarmasdan, bazani shu holatda deb belgilaymiz
uv run alembic stamp head

versions/ papkasida yangi fayl paydo bo'ladi. Uni ochib o'qing — ichida upgrade() (o'zgarishni qo'llash) va downgrade() (orqaga qaytarish) funksiyalari bor, upgrade() da esa ikkala jadvalning create_tablei turishi kerak. stamp jadvallarni qayta yaratmaydi, faqat bazadagi alembic_version jadvaliga "boshlang'ich holat qo'llangan" deb yozadi — ma'lumotlaringiz joyida qoladi. Yangi, bo'sh bazada esa uv run alembic upgrade head hamma jadvalni noldan yaratadi.

head — "eng oxirgi migratsiyagacha" degani. Alembic bazada alembic_version degan kichik jadval ochib, qaysi migratsiyada turganini o'sha yerda eslab qoladi.

Alembic'ga o'tgach, jadvallarni endi faqat u boshqaradi. 1-qismdagi lifespan ichidagi init_db() chaqiruvini olib tashlang: aks holda yangi serverda ilova avval create_all bilan jadvallarni yaratadi, keyingi alembic upgrade head esa table doctor already exists bilan yiqiladi — sinovda shunday bo'ldi.

Python
# app/main.py — lifespan endi bo'sh (kerak bo'lsa boshqa ishlar uchun qoladi)
@asynccontextmanager
async def lifespan(app: FastAPI):
    yield

Endi haqiqiy o'zgarish#

Modelga qabul narxini qo'shamiz:

Python
# app/models.py
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
    # ← yangi maydon. default — Python obyektlari uchun,
    # server_default — bazada allaqachon turgan qatorlar uchun
    price: int = Field(default=0, sa_column_kwargs={"server_default": "0"})

server_default ni tashlab ketmang. Oddiy Field(default=0) faqat Python tomonida ishlaydi — Alembic ustunni nullable=False qilib, bazaga qiymatsiz qo'shishga urinadi. Jadvalda yozuvlar bo'lsa, bu imkonsiz: sinovda upgrade Cannot add a NOT NULL column with default value NULL bilan yiqildi (PostgreSQL ham qatorlar bor jadvalda xuddi shunday rad etadi). server_default bilan esa mavjud qabullar price = 0 ni oladi.

Va ish tartibi har doim bir xil uch qadam:

Terminal
# 1) farqni avtomatik aniqlash
uv run alembic revision --autogenerate -m "appointment'ga narx qo'shildi"

# 2) yaratilgan faylni O'QIB CHIQISH (pastdagi ogohlantirishga qarang)

# 3) qo'llash
uv run alembic upgrade head

Xato qildingizmi — bir qadam orqaga qaytish ham bor: alembic downgrade -1.

Autogenerate yordam beradi, lekin oxirgi so'z sizniki

--autogenerate modellaringiz bilan bazaning hozirgi holatini solishtirib, farqini migratsiya qilib yozadi. Lekin u hamma narsani to'g'ri tushunavermaydi. Masalan, siz ustun nomini o'zgartirsangiz, Alembic buni "eski ustunni o'chir, o'rniga yangisini yarat" deb yozib qo'yadi — natijada o'sha ustundagi butun ma'lumot yo'qoladi. Ba'zi standart qiymatlar va cheklovlarni ham sezmay o'tkazib yuboradi. Shuning uchun qoida oddiy: Alembic yozgan migratsiya faylini ishga tushirishdan oldin doim o'qib chiqing. Ichida drop_column yoki drop_table ko'rsangiz esa alohida to'xtang — bu ko'pincha siz xohlagan narsa emas, balki Alembic noto'g'ri taxmin qilganining belgisi.

SQLite bilan ishlayotganlarga bitta sozlama#

SQLite ALTER TABLEning hamma turini bilmaydi. Zamonaviy SQLite ustun qo'shish va o'chirishni eplaydi, lekin ustun turini yoki cheklovini o'zgartirishni bilmaydi: sinovda price ni int dan str ga o'zgartiruvchi migratsiya near "ALTER": syntax error bilan yiqildi. Yechim — sozlash bo'limida env.py ga qo'shgan argumentimiz:

Python
# migrations/env.py — ikkala context.configure(...) chaqiruvida
    context.configure(
        connection=connection,
        target_metadata=target_metadata,
        render_as_batch=True,  # SQLite uchun: jadvalni qayta qurish orqali ALTER
    )

Shu rejimda Alembic o'zgarishni batch_alter_table bilan yozadi va "yangi jadval yasab, ma'lumotni ko'chirib, eskisini o'chirish" usulida bajaradi — xuddi shu migratsiya muvaffaqiyatli o'tdi. SQLite'da boshqa iloj yo'q, PostgreSQL'da esa batch rejim oddiy ALTER TABLE bilan ishlaydi.

Productionda qayerda turadi#

Migratsiya lokal o'yinchoq emas, deploy zanjirining doimiy qadami. CI/CD maqolasida ko'rgan deploy skriptida Django'ning migrate buyrug'i turgan edi — FastAPI loyihasida o'sha o'rinni shu buyruq egallaydi:

Terminal
# scripts/deploy.sh ichida, xizmatni restart qilishdan OLDIN:
uv run alembic upgrade head

Tartib muhim: avval baza yangi holatga keladi, keyin yangi kod ishga tushadi.

Yakun#

Alembic bilan ishlash aslida uch buyruqqa sig'adi: revision --autogenerate o'zgarishni topib migratsiya yozadi, siz o'sha faylni o'qib to'g'riligini tasdiqlaysiz, upgrade head esa uni bazaga qo'llaydi. Bularning ichida eng muhimi — o'rtadagi o'qish qadami. Chunki Alembic yozgan migratsiya siz nimani xohlaganingizni emas, u kod o'zgarishidan nimani tushunganini yozadi. Ko'pincha bu ikkisi bir xil bo'ladi, lekin ba'zan farq qiladi — va o'sha farq ba'zida butun bir ustundagi ma'lumot yo'qolishiga olib keladi. Ishga tushirishdan oldingi bir daqiqalik o'qish sizni ana shundan saqlaydi.

Shu bilan FastAPI juftligi yakunlandi. Keyingi o'quv maqolalari Django tomonga o'tadi: pytest-django bilan birinchi testni yozish — test nima uchun kerak degan savoldan ishlaydigan test faylgacha.

8 daqiqa 782 marta o‘qildi