Alembic ve Azure SQL ile Python'da veritabanı migrasyonları
Selamlar, bu yazımda Python projelerinde şema değişikliklerini Alembic ile nasıl disiplin altına aldığımıza bakacağız. Hedef veritabanı olarak Azure SQL Database'i seçeceğim, ama anlattıklarımın büyük kısmı PostgreSQL ya da MySQL için de aynen geçerli. Hadi başlayalım.
Production'da elle ALTER TABLE çekmenin sonu hep aynı: ortamlar arasında schema drift, gece yarısı 'staging'de neden bu kolon var lan?' soruları ve commit geçmişi olmayan değişiklikler. Alembic, SQLAlchemy ekibinin migration aracı; her değişikliği upgrade ve downgrade fonksiyonları olan bir Python dosyasına çeviriyor. Yani şemanın versiyon kontrolü artık migration script'leri.
Alembic nedir?
Alembic (Türkçesiyle pek tutmadı, kısaca migrasyon aracı diyelim) SQLAlchemy modellerinizden yola çıkarak SQL üretir, bunu sıralı revision'lar halinde tutar ve veritabanında alembic_version adlı küçük bir tabloda 'şu an hangi sürümdeyim' bilgisini saklar. İleri gidersin (upgrade), geri alırsın (downgrade). Bu kadar.
Kurulum ve bağlantı
Projeye gerekli paketleri kuruyoruz. Azure SQL için pyodbc ve makinenizde Microsoft ODBC Driver 18 kurulu olmalı, bunu peşinen söyleyeyim, ben ilk denediğimde driver eksikliğinden saatler kaybetmiştim:
pip install sqlalchemy alembic pyodbc python-dotenv
Bağlantı string'ini .env içine koyuyoruz. Şifredeki @ karakterini %40 olarak URL-encode etmek zorundayız, yoksa parser yanlış host yakalıyor:
DATABASE_URL=mssql+pyodbc://sqladmin:SecureP%40ss123!@alembic-sql-server.database.windows.net:1433/AlembicDemo?driver=ODBC+Driver+18+for+SQL+Server&Encrypt=yes
Modelleri tanımlamak
Alembic'in --autogenerate özelliği SQLAlchemy modellerinizi okuyarak fark üretir. Yani önce modeller, sonra migration:
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import DeclarativeBase
from datetime import datetime, timezone
class Base(DeclarativeBase):
pass
class Customer(Base):
__tablename__ = 'customers'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(200), nullable=False)
email = Column(String(200), nullable=False, unique=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
env.py'yi yapılandırmak
alembic init alembic komutu bir klasör ve alembic.ini üretir. Asıl iş alembic/env.py içinde: hem modelleri import edeceğiz hem de bağlantı string'ini .env'den okutacağız.
import os
from dotenv import load_dotenv
from sqlalchemy import engine_from_config, pool
from alembic import context
from app.models import Base
load_dotenv()
config = context.config
config.set_main_option('sqlalchemy.url', os.getenv('DATABASE_URL'))
target_metadata = Base.metadata
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
compare_type=True kısmı önemli; bunu eklemezseniz String(100) ile String(200) farkını autogenerate görmez. Bence varsayılan olarak açık olmalıydı.
Migration üretmek ve uygulamak
İlk migration'ı modellerden otomatik çıkarıyoruz, sonra Azure SQL'e doğru sürüyoruz:
alembic revision --autogenerate -m 'create initial tables'
alembic upgrade head
alembic current
Üretilen dosyayı mutlaka açıp okuyun. Autogenerate çoğu zaman doğru iş çıkarır ama her zaman değil. Index isimleri, server default'ları, check constraint'ler - bunlarda gözünüz olsun.
Veri migrasyonu - sahnenin asıl yıldızı
Yeni bir kolon ekleyip eski satırlara mantıklı bir varsayılan vermek, autogenerate'in beceremediği klasik iştir. op.execute ile araya raw SQL sıkıştırıyoruz:
def upgrade() -> None:
op.add_column('customers', sa.Column('tier', sa.String(50), server_default='basic'))
op.execute(
"""
UPDATE customers SET tier = 'premium'
WHERE id IN (
SELECT customer_id FROM orders
GROUP BY customer_id
HAVING SUM(total_amount) > 1000
)
"""
)
Sık karşılaşılan tuzaklar
compare_type=Trueunutmak: Kolon tipi değişiklikleri sessizce kaçar, prod'da kolon hâlâ eski boyutta kalır.- Şifrede özel karakter URL-encode etmemek:
@,!,#gibi karakterler bağlantı string'ini parçalar.urllib.parse.quote_plusdostunuz. - Autogenerate'i kontrol etmeden commit'lemek: Üretilen dosya bazen tabloları yanlış sırayla siler ya da index'i atlar. Açıp okumadan PR'a koymayın.
downgradeboş bırakmak: O an gerek olmayabilir ama bir gün lazım olduğunda yazılmamış downgrade en pahalı borç.
SQL üretmek (offline mod)
DBA ekibi 'önce SQL'i göreyim' diyorsa Alembic'i ofline modda çalıştırıp script üretebiliriz:
alembic upgrade head --sql > migration.sql
Bu özellik kurumsal Azure ortamlarında çok işe yarar; tecrübeyle sabittir.
Kapanış
Bu yazıda Alembic'i Azure SQL ile beraber kurduk, autogenerate'in nereye kadar yetip nerede el değmesi gerektiğine baktık ve sık karşılaşılan tuzakları gördük. Şahsi kanaatim, ilişkisel veritabanı kullanan her Python projesinde Alembic'in başlangıç maliyeti, ileride tasarruf ettireceği zamanın yanında çok küçük kalıyor. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.
