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.
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#
uv add alembic
uv run alembic init migrations
Ikkinchi buyruq loyihada yangi papka va fayl ochadi:
.
├── 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:
# 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:
# 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:
# 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:
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.
# 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:
# 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:
# 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:
# 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:
# 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.