EF Core Migration'larını Production'da Güvenle Uygulamak
Yazar: Oğuzhan KARAGÜZEL ·
İşte projelerimizde DB First ilerliyoruz. Tablo değişikliğini önce test veritabanında bir SQL scriptiyle yapıyorum, kodu ona göre güncelliyorum, testler geçiyor. Sonra yayın günü geliyor. Uygulamayı canlıya alıyorum ve uygulama çöküyor. Sebep ise her seferinde aynı: scripti test veritabanında çalıştırdım ama canlıda çalıştırmayı unuttum.
Genelde birkaç dakikada çözüyorum. Scripti canlıda çalıştırıyorum, uygulama ayağa kalkıyor. Ama o birkaç dakika boyunca proje hata veriyor. Hatanın sebebi teknik bir problem değil, benim unutkanlığım.
Asıl sorun şu: kod bir yoldan, tablo başka bir yoldan yayına çıkıyor. İkisini birbirine bağlayan tek şey benim hafızam. Bu yazıda EF Core'un migration araçlarıyla bu bağı nasıl koda dönüştürebileceğimizi, hangi yöntemin ne zaman uygun olduğunu ve production'da nelere dikkat etmek gerektiğini anlatacağım.
Migration aslında ne işe yarar?
Code First'te modelde yaptığımız her değişiklik bir migration sınıfına dönüşür. Bu sınıfın iki metodu vardır: Up değişikliği uygular, Down geri alır. EF Core hangi migration'ların uygulandığını veritabanındaki __EFMigrationsHistory tablosuna yazar.
Bu tablo sayesinde veritabanı hangi değişikliklerin uygulandığını biliyor. "Canlıda bu script çalıştı mı?" sorusunun cevabı artık benim hafızamda değil, veritabanında.
Migration oluşturmanın ve yönetmenin temellerini EF Core Code First Migration Yönetimi yazısında anlattım. DB First, Code First ve ikisinin karışımı arasında nasıl seçim yaptığımı da EF Core İlişkiler ve Mixed Yaklaşım yazısında bulabilirsiniz. Bu yazıda işin canlı tarafına odaklanıyorum.
Dört yöntem
EF Core, migration'ları canlıya taşımak için dört yol sunuyor.
| Yöntem | Nasıl çalışır | Ne zaman uygun |
|---|---|---|
Database.Migrate() | Uygulama açılırken bekleyen migration'ları uygular | Tek sunucu, veritabanını uygulamanın yönettiği projeler |
dotnet ef database update | Geliştirici makinesinden ya da CI'dan doğrudan uygular | Geliştirme ve test ortamları |
| SQL script | Migration'lar SQL dosyasına çevrilir, elle ya da pipeline'da çalıştırılır | Değişikliği uygulamadan önce birinin incelemesi gerekiyorsa |
| Migration bundle | Migration'lar tek bir çalıştırılabilir dosyaya paketlenir | CI/CD hatları, Docker |
Microsoft'un dokümanı production için SQL script ya da bundle öneriyor. Ben kendi projem Elevare'de Migrate() kullanıyorum. Nedenini aşağıda anlatacağım.
Uygulama açılırken Migrate()
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync();Bu yöntemi seçmemin sebebi, kontrolün bende olduğunu hissetmem. Uygulama ayağa kalktığında tabloların kodla uyumlu olduğundan eminim. Girişte anlattığım unutkanlık burada yaşanamaz, çünkü kod canlıya çıktığı anda tablo değişiklikleri de onunla birlikte geliyor.
Tabii bu yöntemin eksileri de var:
- Yetki: Uygulamanın veritabanı kullanıcısı tablo oluşturup silebilmeli. Normalde bir uygulamaya bu kadar yetki vermek istemeyiz.
Birden fazla sunucu: İki sunucu aynı anda açılırsa ikisi de migration çalıştırmaya kalkabilir. EF Core 9'dan beri migration'lar başlamadan önce veritabanında bir kilit alınıyor. PostgreSQL'de logu açınca bu satır görülür:
ÇıktıLOCK TABLE "__EFMigrationsHistory" IN ACCESS EXCLUSIVE MODEİkinci sunucu bu kilidin açılmasını bekliyor, sonra uygulanacak bir şey kalmadığını görüp devam ediyor. Yine de kilit beklerken açılış uzayabilir.
- Hata olursa: Migration yarıda kalırsa uygulama hiç açılmıyor. Açılış süresi de uzuyor; Docker kullanıyorsanız sağlık kontrolünün bekleme süresini buna göre ayarlamanız gerekiyor.
Elevare tek sunucuda çalışıyor ve veritabanını yalnızca uygulamanın kendisi kullanıyor. Bu şartlarda Migrate() benim için en sade yol. Ama her projede bunu kullanmıyorum. Birden fazla sunucuda çalışan ya da her değişikliğin bir DBA'den onay alması gereken bir projede script ya da bundle kullanırım. Hangisini seçeceğime projenin ihtiyacına bakarak karar veriyorum.
SQL script üretmek
dotnet ef migrations script --idempotent -o migrate.sqlBu komut bütün migration'ları tek bir SQL dosyasına çeviriyor. Buradaki --idempotent parametresi önemli: her migration'ın başına "bu zaten uygulanmış mı?" kontrolü ekliyor. PostgreSQL için üretilen script'ten bir parça:
START TRANSACTION;
DO $EF$
BEGIN
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20261103004538_RenameName') THEN
ALTER TABLE "Customers" RENAME COLUMN "Name" TO "FullName";
END IF;
END $EF$;
DO $EF$
BEGIN
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20261103004538_RenameName') THEN
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20261103004538_RenameName', '10.0.14');
END IF;
END $EF$;
COMMIT;Her migration kendi transaction'ı içinde. Önce __EFMigrationsHistory tablosuna bakılıyor, migration orada yoksa değişiklik yapılıyor ve tabloya kaydediliyor. Yani aynı scripti iki kez çalıştırsanızda bir şey bozulmaz. Hangi migration'ların uygulandığını bilmediğiniz bir veritabanında bile rahatlıkla kullanabilirsiniz.
Belirli bir aralık istersem:
dotnet ef migrations script AddOrders AddInvoicesBu yöntemin en büyük artısı, değişiklik canlıya gitmeden önce birinin SQL'i okuyabilmesi.
Migration bundle
dotnet ef migrations bundle --self-contained -r linux-x64
./efbundle --connection "Host=...;Database=...;Username=...;Password=..."Bundle, migration'ları tek bir çalıştırılabilir dosyaya paketliyor. Kendi makinemde Windows için ürettiğim bundle'ı çalıştırınca çıktı şöyleydi:
Acquiring an exclusive lock for migration application.
Applying migration '20261103004538_RenameName'.
Applying migration '20261103004542_DropPhone'.
Applying migration '20261103004552_RenameAndLimit'.
Done.Aynı bundle'ı ikinci kez çalıştırdığımda:
No migrations were applied. The database is already up to date.
Done.--self-contained ile üretilen dosya .NET çalışma zamanını da içinde taşıyor, o yüzden sunucuda .NET kurulu olması gerekmiyor. Bunun bedeli boyut: benim küçük örneğimde bile dosya 107 MB oldu. Sunucuda .NET zaten kuruluysa bu parametreyi kullanmayabilirsiniz.
CI'da üretip yayın adımında uygulamadan önce çalıştırmak için çok uygun. Bu yöntemde uygulamanın tablo oluşturma yetkisine ihtiyacı kalmıyor. O yetki yalnızca bundle'ı çalıştıran kullanıcıda oluyor.
Veri kaybettiren değişiklikler
EF Core bir kolonun silineceğini fark ettiğinde migration oluştururken uyarıyor:
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.Bu uyarıyı hiçbir zaman geçiştirmeyin. Ne kadar kolay veri kaybedilebileceğini görmek için küçük bir deneme yaptım.
Customer sınıfındaki Name property'sinin adını FullName yaptım. EF bunu doğru anladı ve migration'a şunu yazdı:
migrationBuilder.RenameColumn(
name: "Name",
table: "Customers",
newName: "FullName");Sonra aynı anda iki şey değiştirdim: adını DisplayName yaptım ve [MaxLength(200)] ekledim. Bu sefer EF ad değişikliğini tanıyamadı:
migrationBuilder.DropColumn(
name: "FullName",
table: "Customers");
migrationBuilder.AddColumn<string>(
name: "DisplayName",
table: "Customers",
type: "character varying(200)",
maxLength: 200,
nullable: false,
defaultValue: "");Tabloya önceden "Ayşe Yılmaz" adında bir müşteri eklemiştim. Migration'ı uyguladıktan sonra tablo şöyleydi:
Id | DisplayName
----+-------------
1 |İsim gitmişti. Uyarı da bunu söylüyordu ama migration hatasız uygulandı. Doğrusu, migration dosyasını elle düzeltip DropColumn ve AddColumn yerine RenameColumn ve ardından AlterColumn yazmak. Bu yüzden her migration dosyasını uygulamadan önce açıp okuyorum. Bir de değişiklikleri küçük tutmaya çalışıyorum: ad değişikliği bir migration, tip değişikliği başka bir migration.
Veriyi taşımam gerektiğinde migrationBuilder.Sql() kullanıyorum. Elevare'de önizleme taslaklarını ayrı kolonlara taşırken böyle bir migration yazdım. Önce yeni kolonları ekledim. Sonra onay bekleyen bir isteği olmayan sayfalarda eski kolondaki veriyi yenisine kopyaladım ve eskisini boşalttım. Bu, bir sonraki bölümdeki yöntemin küçük bir örneği.
Kesintisiz yayın: genişlet ve daralt
Bazen eski kodun yeni tabloyla, yeni kodun da eski tabloyla bir süre birlikte çalışması gerekir. Böyle durumlarda değişikliği üç adıma bölüyorum:
- Genişlet: Yeni kolonu ekle, eskisine dokunma. Veriyi kopyala.
- Geçir: Kodu yeni kolonu kullanacak şekilde yayınla.
- Daralt: Her şey yeni kolona geçtikten sonra, ayrı bir migration'la eski kolonu sil.
Bu yöntem daha fazla işe sebep oluyor . Ama hiçbir adımda veri kaybı ya da çökme riski yok.
Geri alma
dotnet ef database update OncekiMigration
dotnet ef migrations script SimdikiMigration OncekiMigrationİlk komut değişikliği doğrudan geri alıyor, ikincisi geri alma scriptini üretiyor. Ama şunu bilmek lazım: kolon silen bir migration'ı geri almak kolonu geri getirir, içindeki veriyi getirmez.
Yukarıdaki denemede iki migration'ı geri aldım:
Reverting migration '20261103004552_RenameAndLimit'.
Reverting migration '20261103004542_DropPhone'.
Done.Kolonlar geri geldi, veriler gelmedi:
Id | FullName | Phone
----+----------+-------
1 | |Bu yüzden canlıda asıl güvencem Down metodu değil, migration'dan önce aldığım yedek. Bir şey ters giderse çoğu zaman geri almak yerine yeni bir düzeltme migration'ı yazıp ileriye doğru gidiyorum.
DB First kullanıyorsanız
EF Core'un migration araçları Code First için. Ama girişteki unutkanlık DB First'te de çözülebilir:
- Tablo değişikliği scriptlerini repoda numaralı dosyalar olarak tutun.
- Hangi scriptin uygulandığını bir tabloya kaydedin, yani
__EFMigrationsHistorytablosunun yaptığı işi siz yapın. DbUp gibi küçük kütüphaneler bunu hazır olarak sunuyor. - Uygulama açılırken veritabanının beklenen sürümde olup olmadığını kontrol edin. Değilse anlaşılır bir hata verin.
Böylece "canlıda çalıştırdım mı?" sorusunun cevabı yine hafızanızda değil, veritabanında olur.
Kontrol listesi
- Migration dosyasını okudum. Veri kaybı uyarısı yok ya da bilerek kabul ettim.
- Test veritabanında uyguladım ve uygulamayı orada çalıştırdım.
- Canlı veritabanının yedeğini aldım.
- Hangi yöntemle uygulayacağım belli:
Migrate(), script ya da bundle. - Bir şey ters giderse ne yapacağım belli: yedekten dönüş ya da düzeltme migration'ı.

