Alembic ile Veritabanı Migration Yönetimi
Selamlar, bu yazımda SQLAlchemy ile Python uygulaması yazarken er ya da geç karşımıza çıkan migration yönetimini Alembic üzerinden anlatacağım. Özellikle autogenerate'in sessizce atladığı şeylere ve production'da uzun lock'a yol açmadan göç ettirmeye gireceğim.
Şema değişmek zorunda: yeni bir kolon, kaldırılan bir index, ikiye bölünen bir tablo. Elle SQL yazıp 'prod'da koştum sanırım' demek bir noktadan sonra dert getiriyor. Migration tool'u tam burada işin sigortası.
Alembic nedir?
Alembic, SQLAlchemy ekibinin yazdığı küçük ve odaklı bir migration aracı. Şema değişikliklerini sürümlü Python script'leri olarak tutuyor, veritabanında alembic_version tablosunda hangi revision'da olduğumuzu takip ediyor. Django ya da Rails kadar 'her şey dahil' değil; bence bu da onu tercih edilebilir kılan şey.
Kurulum ve baslangic
PostgreSQL ile çalıştığım için psycopg2-binary'i de ekliyorum:
pip install alembic sqlalchemy psycopg2-binary
alembic init alembic
alembic.ini içindeki sqlalchemy.url satırını boş bırakıp bağlantı URL'ini env.py'den environment variable olarak okumak benim tercihim - prod secret'ı .ini dosyasında dolaşmasın.
env.py: autogenerate'in kalbi
alembic/env.py en kritik yer. Modellerinizin Base.metadata'sını buraya bağlamazsanız autogenerate boş üretir:
from app.models import Base
target_metadata = Base.metadata
def run_migrations_online() -> None:
configuration = config.get_section(config.config_ini_section)
configuration['sqlalchemy.url'] = os.environ['DATABASE_URL']
connectable = engine_from_config(
configuration, 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()
Buradaki iki bayrak hayat kurtarıyor: compare_type=True kolon tipi, compare_server_default=True server default farklarını yakalatıyor. Default'ları kapalı; bu yüzden autogenerate sessizce atlar.
Autogenerate'in atladiklari
Şimdi acı tarafa gelelim. alembic revision --autogenerate -m '...' modele ve veritabanına bakıp farkı yazıyor. Ama bunu gözü kapalı commit etmeyin; şu durumları kaçırır:
- Tablo yeniden adlandırması: 'users'ı 'accounts'a çevirdiğinizde Alembic eskisini drop, yenisini create eder. Veri uçar.
- Kolon adı değişikliği: Aynı şey kolonda da olur.
op.alter_column(... new_column_name='...')elle yazılmalı. - Server-side default:
default=Python tarafında,server_default=veritabanı tarafında. İkisini karıştırırsanız autogenerate kafayı yer.
Üretilen dosyayı her zaman açıp okumak gerekiyor. İki dakikalık göz gezdirme, prod'daki iki saatlik incident'tan iyidir.
Downgrade yazmak
Her upgrade()'in karşılığında bir downgrade() yazmak Alembic'in vaadi ama data migration yapan bir revision'da downgrade nadiren tam tersini yapabilir. Ben şöyle yapıyorum: şema değişikliği ile data değişikliğini ayrı revision'lara koyuyorum. Şema migration'ları reversible kalıyor; data migration'larında downgrade'i pass bırakıp 'one-way' olduğunu commit mesajında yazıyorum.
Coklu head ve merge
Takımda iki kişi aynı down_revision üzerinden revision yarattığında repo iki head'li hale gelir. alembic heads iki satır gösterir. Panik yok, çözüm tek satır:
alembic merge -m 'merge feature branches' abc123 def456
Bu komut boş bir merge revision yaratır ve iki head'i birleştirir. Ben merge revision'larında asla data taşımam, kısa tutarım.
Online migration: production tuzaklari
En kritik kısma geldik. Production PostgreSQL'de sade bir CREATE INDEX tabloya yazma kilidi koyar; trafik yoğunsa uygulamanız donar. Alembic bu konuda hediye sunmuyor, doğru SQL'i siz yazacaksınız:
def upgrade() -> None:
# CREATE INDEX CONCURRENTLY transaction icinde calismaz.
with op.get_context().autocommit_block():
op.create_index(
'ix_posts_published_at', 'posts', ['published_at'],
postgresql_concurrently=True, if_not_exists=True,
)
autocommit_block transaction'dan çıkıyor, postgresql_concurrently=True index'i bloklamadan kuruyor. NOT NULL kolon eklerken de aynı dikkat: önce nullable ekleyin, batch'lerle doldurun, sonra ayrı migration'da NOT NULL koyun. Tek migration'da hepsi olsun derseniz, tablo büyükse lock kuyruğu birikir.
Sik karsilasilan tuzaklar
compare_typevecompare_server_defaultbayraklarini unutmak: env.py'de yoksa autogenerate yarısını görmez, prod ile dev şemaları sessizce ayrılır.alembic upgrade headkomutunu CI dışında çalıştırmak: Migration'lar deploy pipeline'ının parçası olmalı; bir geliştirici lokalde unutursa staging ile prod ayrışır.- Downgrade'i hiç test etmemek: Staging'de bir kez deneyip geçersiniz, geri kalanı güvene kalır. Kötü sürpriz.
- Buyuk tablolarda CONCURRENTLY'i atlamak: Saniyelik bir
CREATE INDEXsaatlerce uzayan lock kuyruğuna dönüşebilir. - Migration icinde ORM modelini import etmek: Bugünkü
Usermodeli altı ay sonra çoktan değişmiş olur. Migration içinde sadeceopve düz SQL ya dasa.Tablekullanın.
Dogrulama
Migration'ı uygulamadan önce planı görmek için:
alembic upgrade head --sql > /tmp/migration.sql
Bu komut SQL'i veritabanına dokunmadan üretir. CREATE INDEX satırlarında CONCURRENTLY var mı, beklenmeyen bir DROP TABLE var mı diye bakmak iki dakikalık iş.
Kapanis
Bu yazıda Alembic'in temellerini, autogenerate'in tuzaklarını, çoklu head'leri ve prod'da bloksuz migration için CONCURRENTLY kalıbını gördük. Şahsi kanaatim, autogenerate çıktısını her seferinde gözle okumak ve şema ile data değişikliklerini ayrı migration'lara bölmek - bu iki alışkanlık başınıza gelecek dertlerin yarısını savar. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.
