Migration pada Entity Framework Core adalah mekanisme untuk melacak dan menerapkan perubahan skema database secara terstruktur dan dapat diulang, memungkinkan struktur database berevolusi seiring perubahan model C# tanpa perlu mengubah database secara manual lewat script SQL terpisah.

Setiap migration merepresentasikan satu set perubahan skema, seperti penambahan tabel baru atau kolom tambahan, yang disimpan sebagai file C# berisi instruksi Up (menerapkan perubahan) dan Down (membatalkan perubahan), memungkinkan riwayat perubahan database terlacak jelas seperti riwayat commit pada version control.

Artikel ini membahas cara membuat dan menerapkan migration, serta seeding data untuk mengisi data awal yang dibutuhkan aplikasi, seperti data master atau akun administrator default, secara otomatis saat database pertama kali dibuat.

Membuat Migration Pertama

Perintah dotnet ef migrations add membuat file migration baru

dotnet ef migrations add InitialCreate -- Expected: menghasilkan folder Migrations/ berisi -- file C# yang mendefinisikan skema tabel awal -- berdasarkan DbSet yang didefinisikan di DbContext
Perintah dotnet ef migrations add membandingkan model C# saat ini dengan snapshot migration terakhir, lalu menghasilkan file migration baru berisi instruksi perubahan skema yang dibutuhkan. Migration pertama biasanya menghasilkan instruksi pembuatan seluruh tabel berdasarkan DbSet yang sudah didefinisikan di DbContext.

Menerapkan Migration ke Database

dotnet ef database update menerapkan migration ke database sesungguhnya

dotnet ef database update -- Expected: database dibuat (jika belum ada) -- dan seluruh migration yang belum diterapkan -- akan dijalankan secara berurutan
Perintah dotnet ef database update benar-benar menjalankan instruksi migration terhadap database, membuat tabel, kolom, atau perubahan skema lain sesuai yang didefinisikan di file migration. Jika database belum ada sama sekali, perintah ini juga akan membuatnya terlebih dahulu sebelum menerapkan migration.

Menambahkan Migration untuk Perubahan Skema Baru

Setiap perubahan model membutuhkan migration baru

// Setelah menambah property baru di model Produk: public class Produk { // ... property lama ... public string Deskripsi { get; set; } // property BARU } dotnet ef migrations add TambahKolomDeskripsi dotnet ef database update
Setiap kali model C# diubah, misalnya menambah property baru, migration baru perlu dibuat untuk melacak perubahan tersebut, lalu diterapkan dengan database update. Praktik ini menjaga agar skema database selalu konsisten dengan model C# dan setiap perubahan terdokumentasi dengan jelas lewat nama migration yang deskriptif.

Seeding Data Awal dengan HasData

HasData mendefinisikan data awal yang disertakan dalam migration

protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Kategori>().HasData( new Kategori { Id = 1, Nama = "Elektronik" }, new Kategori { Id = 2, Nama = "Pakaian" } ); }
Method HasData di dalam OnModelCreating memungkinkan data awal didefinisikan langsung di kode dan akan disertakan sebagai bagian dari migration, sehingga data master seperti daftar kategori akan otomatis tersedia begitu migration diterapkan ke database baru, tanpa perlu script INSERT terpisah yang dijalankan manual.

Idempotent Migration dan Menangani Migration yang Sudah Terlanjur Jalan

Membuat script migration idempotent untuk deployment ke server production

# Membuat script SQL idempotent dari seluruh migration (aman dijalankan berkali-kali) dotnet ef migrations script --idempotent -o deploy_migration.sql # Melihat daftar migration dan status penerapannya di database target dotnet ef migrations list # Jika migration tertentu sudah dijalankan manual lewat SQL, # tandai sebagai applied tanpa menjalankan ulang DDL-nya: dotnet ef database update NamaMigrationTerakhirYangValid

Pada tim dengan banyak developer, migration EF Core sering dibuat paralel oleh beberapa orang sehingga urutan penerapannya di database production perlu dipastikan aman walau dijalankan berulang kali atau di server yang sudah punya sebagian migration ter-apply. Perintah dotnet ef migrations script --idempotent menghasilkan file SQL yang membungkus setiap perubahan dengan pengecekan pada tabel __EFMigrationsHistory, sehingga migration yang sudah pernah dijalankan otomatis dilewati tanpa menimbulkan error duplikasi kolom atau tabel.

Script idempotent ini jauh lebih aman dipakai pada pipeline CI/CD dibandingkan menjalankan dotnet ef database update langsung di server production, karena tim operasional bisa meninjau isi SQL sebelum dieksekusi, dan script yang sama bisa dijalankan ulang tanpa risiko merusak skema yang sudah ada jika deployment gagal di tengah jalan dan perlu diulang.

Situasi yang kerap membingungkan adalah ketika perubahan skema sudah terlanjur dieksekusi manual lewat SQL Server Management Studio tanpa melalui migration EF Core, sehingga skema database dan riwayat migration menjadi tidak sinkron. Pada kasus ini, tabel __EFMigrationsHistory bisa diperbarui secara manual untuk menandai migration tertentu sudah diterapkan, atau menggunakan dotnet ef database update dengan menunjuk migration yang sesuai agar EF Core tidak mencoba menjalankan ulang DDL yang sudah ada.

Seeding Data: HasData di Migration vs Seed Manual Saat Startup

HasData menghasilkan migration tetap; seed manual berjalan setiap aplikasi start

// Pendekatan 1: HasData di OnModelCreating, ikut masuk ke file migration protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Role>().HasData( new Role { Id = 1, Name = "Admin" }, new Role { Id = 2, Name = "Staff" } ); } // Pendekatan 2: seed manual dijalankan sekali saat aplikasi start using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); if (!db.Roles.Any()) { db.Roles.AddRange(new Role { Name = "Admin" }, new Role { Name = "Staff" }); await db.SaveChangesAsync(); } } // Pendekatan 2 cocok untuk data yang butuh logika kondisional sebelum disimpan

HasData yang dipanggil di dalam OnModelCreating membuat EF Core menghasilkan baris INSERT eksplisit di dalam file migration itu sendiri, sehingga data referensi seperti daftar role atau kategori tetap konsisten di semua environment karena ikut tercatat sebagai bagian dari riwayat skema database. Pendekatan ini cocok untuk data statis yang jarang berubah dan nilainya sudah pasti sejak awal pengembangan, seperti master data role atau status pesanan. Primary key pada data HasData harus ditentukan secara eksplisit, karena EF Core tidak bisa mengandalkan nilai identity otomatis saat membuat skrip migration.

Untuk data yang membutuhkan logika kondisional, seperti mengecek apakah tabel sudah berisi data sebelum insert, atau nilai yang bergantung pada hasil pemanggilan service lain, pendekatan seed manual yang dijalankan di Program.cs lewat scope sementara (CreateScope) lebih fleksibel dibanding HasData yang sifatnya statis dan dievaluasi saat compile time migration. Scope manual ini diperlukan karena DbContext bersifat Scoped sedangkan kode startup di Program.cs berjalan di luar konteks request. Pendekatan ini juga memungkinkan pemanggilan service lain seperti IPasswordHasher sebelum data benar-benar disimpan ke database.

Kombinasi keduanya sering dipakai dalam satu project: HasData untuk master data yang benar-benar tetap seperti daftar role, dan seed manual untuk data awal yang lebih dinamis seperti akun admin default yang passwordnya perlu di-hash terlebih dahulu sebelum disimpan. Memilih pendekatan yang salah, misalnya memakai HasData untuk data yang sering berubah, akan memaksa pembuatan migration baru setiap kali nilai seed tersebut perlu diperbarui. Tim yang paham perbedaan ini bisa menghindari migration yang menumpuk hanya untuk mengubah data referensi yang sebetulnya lebih cocok dikelola lewat seeding manual.

Menambahkan Concurrency Token agar Update Bersamaan Tidak Saling Menimpa

public class Produk { public int Id { get; set; } public decimal Stok { get; set; } [Timestamp] public byte[] RowVersion { get; set; } } // Migration menambahkan kolom RowVersion // EF Core otomatis menyisipkan RowVersion lama di klausa WHERE saat UPDATE, // melempar DbUpdateConcurrencyException jika data sudah berubah di baris lain

Tanpa concurrency token, dua proses yang membaca lalu menyimpan baris data yang sama bisa saling menimpa perubahan satu sama lain tanpa ada peringatan apa pun, dan proses yang menyimpan belakangan akan menang begitu saja. Menambahkan kolom RowVersion bertipe timestamp membuat EF Core secara otomatis memeriksa apakah data masih sama persis seperti saat terakhir dibaca sebelum benar-benar menjalankan perintah UPDATE ke database.

Migration yang menambahkan kolom RowVersion ini perlu dijalankan dengan hati-hati pada tabel yang sudah berisi data produksi, karena nilai awal kolom tersebut akan diisi otomatis oleh mesin database saat migration dijalankan. Saat DbUpdateConcurrencyException muncul, aplikasi perlu menangani kasus tersebut dengan baik, misalnya meminta pengguna memuat ulang data terbaru sebelum mencoba menyimpan perubahannya sekali lagi.

Kesalahan Umum yang Sering Terjadi

Mengedit File Migration yang Sudah Diterapkan ke Production

Mengubah file migration yang sudah diterapkan ke database production bisa menyebabkan ketidaksesuaian antara riwayat migration yang tercatat EF Core dengan skema database sesungguhnya; perubahan lebih lanjut sebaiknya selalu dibuat sebagai migration baru.

Lupa Menjalankan Database Update Setelah Membuat Migration

Membuat file migration dengan migrations add saja tidak mengubah database sama sekali; perintah database update tetap wajib dijalankan agar perubahan skema benar-benar diterapkan ke database sesungguhnya.

Menghapus Migration Lama Tanpa Memahami Konsekuensinya

Menghapus file migration lama secara sembarangan bisa merusak riwayat perubahan skema yang dibutuhkan EF Core untuk menentukan perubahan apa saja yang perlu diterapkan pada database yang belum ter-update ke versi terbaru.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Migration berbasis kode memberikan jejak audit yang jelas dan bisa diintegrasikan dengan version control, memudahkan tim melacak evolusi skema database seiring waktu bersamaan dengan perubahan kode aplikasi.

Namun untuk database yang sangat besar dan kompleks dengan banyak constraint khusus, beberapa tim memilih mengelola skema database secara terpisah dengan tool migrasi database khusus (database-first) dibanding mengandalkan migration otomatis EF Core (code-first) yang terkadang kurang presisi untuk kasus skema yang sangat spesifik.

Ringkasan

Migration EF Core memberikan cara terstruktur untuk mengevolusikan skema database seiring perubahan model aplikasi, dengan HasData sebagai pelengkap untuk seeding data awal yang dibutuhkan. Selalu buat migration baru untuk setiap perubahan skema, dan hindari mengedit migration yang sudah diterapkan ke environment production.

Seri Belajar .NET Core:

← Sebelumnya: Entity Framework Core untuk Pemula

→ Selanjutnya: Relasi One to Many dan Many to Many di EF Core