MODUL 2 - LEKCIJA 2

EF Migracije

Enable-Migrations, Add-Migration, Update-Database - Upravljanje promjenama u bazi

⏱️ Trajanje: ~3 časa 📚 Nivo: Srednji 🎯 Praktični primjeri: 3

📖 Šta su Migracije?

Entity Framework Migracije omogućavaju vam da upravljate promjenama u strukturi baze podataka kroz kod. Umjesto ručnog pisanja SQL skripti, migracije automatski generišu SQL kod na osnovu promjena u vašim Model klasama.

🔑 Zašto Koristiti Migracije?

  • Version Control: Migracije su C# fajlovi - možete ih commit-ovati u Git
  • Reproducibilnost: Ista migracija će kreirati istu bazu na svim okruženjima
  • Rollback: Možete vratiti promjene ako nešto pođe po zlu
  • Timski Rad: Svaki developer može primijeniti iste promjene
  • Automatski SQL: Ne morate pisati CREATE TABLE, ALTER TABLE, itd. ručno

🔄 Životni Ciklus Migracija i __MigrationHistory

Da biste razumjeli kako Entity Framework zna koje su migracije primijenjene, potrebno je razumjeti njegov unutrašnji mehanizam i tabelu __MigrationHistory.

EF Migrations Lifecycle

Slika: Životni ciklus EF Migracija

Šta je __MigrationHistory?

Prilikom prvog pokretanja Update-Database, EF kreira sistemsku tabelu pod imenom __MigrationHistory u vašoj bazi. Njena uloga je esencijalna:

  • Prati MigrationId (npr. 20231015120000_InitialCreate) svake izvršene migracije.
  • Sadrži Model kolonu (baza čuva binarni potpis vaših entiteta) putem koje EF provjerava jesu li klase i baza sinhronizovane.
  • Sprečava ponovno pokretanje već izvršenih SQL skripti prilikom svakog poziva Update-Database.

🚀 Enable-Migrations: Aktiviranje Migracija

Prije nego što možete koristiti migracije, morate ih omogućiti u vašem projektu. Ovo se radi jednom po projektu.

Koraci za Omogućavanje Migracija

  1. Otvorite Package Manager Console
    • Tools → NuGet Package Manager → Package Manager Console
    • Ili View → Other Windows → Package Manager Console
  2. Izvršite komandu:
    🛠️ Enable-Migrations Komanda
    PM> Enable-Migrations
  3. Provjerite da li je kreiran Migrations folder
    • U Solution Explorer-u trebate vidjeti Migrations/ folder
    • U njemu će biti Configuration.cs fajl

Configuration.cs Fajl

Nakon Enable-Migrations, EF kreira Configuration.cs fajl koji sadrži postavke za migracije.

🛠️ Configuration.cs
// Migrations/Configuration.cs
namespace JavnaUprava.Migrations
{
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<JavnaUprava.Models.JavnaUpravaDbContext>
    {
        public Configuration()
        {
            // Automatske migracije - preporučeno false za produkciju
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(JavnaUprava.Models.JavnaUpravaDbContext context)
        {
            // Seediranje početnih ministarstava
            context.Departments.AddOrUpdate(
                d => d.DepartmentCode,
                new Models.Department { DepartmentName = "Ministarstvo Finansija", DepartmentCode = "MF", Budget = 50000000 },
                new Models.Department { DepartmentName = "Ministarstvo Pravde", DepartmentCode = "MP", Budget = 35000000 }
            );
            
            // Seediranje administratora
             context.Employees.AddOrUpdate(
                e => e.JMBG,
                new Models.Employee 
                { 
                    FirstName = "Admin", 
                    LastName = "Adminić", 
                    JMBG = "1234567890123", 
                    DepartmentID = 1 // Vežemo za MF
                }
            );
        }
    }
}

💡 AutomaticMigrationsEnabled

AutomaticMigrationsEnabled = false znači da morate eksplicitno kreirati migracije. Preporučeno je ostaviti na false jer daje više kontrole i omogućava review migracija prije primjene.

➕ Add-Migration: Kreiranje Migracije

Kada promijenite Model klase (dodate svojstvo, promijenite tip, itd.), morate kreirati novu migraciju koja će zabilježiti te promjene.

Osnovna Sintaksa

🛠️ Add-Migration Komanda
PM> Add-Migration MigrationName

# Primjer:
PM> Add-Migration InitialCreate
PM> Add-Migration AddEmailToEmployee
PM> Add-Migration CreatePostsTable

Primjer: Kreiranje Prve Migracije

Pretpostavimo da imate Model klase Employee i Department koje ste kreirali u prethodnoj lekciji, ali još nema baze podataka.

🛠️ Kreiranje Prve Migracije
# 1. Omogućite migracije (ako već niste)
PM> Enable-Migrations

# 2. Kreirajte prvu migraciju koja će kreirati sve tabele
PM> Add-Migration InitialCreate

EF će kreirati novi fajl u Migrations/ folderu sa nazivom kao što je 20240101120000_InitialCreate.cs (timestamp + ime migracije).

Struktura Migracije Fajla

🛠️ Primjer Migracije Fajla
// Migrations/20240101120000_InitialCreate.cs
namespace JavnaUprava.Migrations
{
    using System;
    using System.Data.Entity.Migrations;

    public partial class InitialCreate : DbMigration
    {
        public override void Up()
        {
            // Kreiranje tabela u 'Stats' shemi
            CreateTable(
                "Stats.Departments",
                c => new
                    {
                        DepartmentID = c.Int(nullable: false, identity: true),
                        DepartmentName = c.String(nullable: false, maxLength: 100),
                        DepartmentCode = c.String(maxLength: 20),
                        Budget = c.Decimal(nullable: false, precision: 18, scale: 2),
                    })
                .PrimaryKey(t => t.DepartmentID);

            CreateTable(
                "Stats.Employees",
                c => new
                    {
                        EmployeeID = c.Int(nullable: false, identity: true),
                        FirstName = c.String(nullable: false, maxLength: 50),
                        LastName = c.String(nullable: false, maxLength: 50),
                        JMBG = c.String(nullable: false, maxLength: 13),
                        DepartmentID = c.Int(nullable: false),
                    })
                .PrimaryKey(t => t.EmployeeID)
                .ForeignKey("Stats.Departments", t => t.DepartmentID, cascadeDelete: true)
                .Index(t => t.DepartmentID);
        }

        public override void Down()
        {
            DropForeignKey("Stats.Employees", "DepartmentID", "Stats.Departments");
            DropIndex("Stats.Employees", new[] { "DepartmentID" });
            DropTable("Stats.Employees");
            DropTable("Stats.Departments");
        }
    }
}

🔑 Up() i Down() Metode

  • Up(): Primjenjuje promjene (kreira tabele, dodaje kolone, itd.)
  • Down(): Vraća promjene (briše tabele, uklanja kolone, itd.)

Down() metoda omogućava rollback - vraćanje na prethodnu verziju baze.

Primjer: Dodavanje Nove Kolone

Pretpostavimo da želite dodati PhoneNumber svojstvo u Employee klasu.

🛠️ 1. Promjena Model Klase
// Models/Employee.cs
public class Employee
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    
    // Nova kolona
    [StringLength(20)]
    public string PhoneNumber { get; set; }
    
    public DateTime HireDate { get; set; }
    public int DepartmentId { get; set; }
    public virtual Department Department { get; set; }
}
🛠️ 2. Kreiranje Migracije
PM> Add-Migration AddPhoneNumberToEmployee
🛠️ 3. Generisana Migracija
// Migrations/20240101130000_AddBudgetToDepartment.cs
public partial class AddBudgetToDepartment : DbMigration
{
    public override void Up()
    {
        AddColumn("Stats.Departments", "Budget", c => c.Decimal(nullable: false, precision: 18, scale: 2));
    }

    public override void Down()
    {
        DropColumn("Stats.Departments", "Budget");
    }
}

💾 Update-Database: Primjena Migracija

Nakon što kreirate migraciju, morate je primijeniti na bazu podataka. Ovo će izvršiti SQL kod iz Up() metode.

Osnovna Sintaksa

🛠️ Update-Database Komande
# Primjenjuje sve migracije koje još nisu primijenjene
PM> Update-Database

# Primjenjuje migraciju do određene migracije
PM> Update-Database -TargetMigration MigrationName

# Vraća bazu na određenu migraciju (rollback)
PM> Update-Database -TargetMigration PreviousMigrationName

# Prikazuje SQL koji će biti izvršen (bez primjene)
PM> Update-Database -Script

# Primjenjuje migraciju na određeni connection string
PM> Update-Database -ConnectionString "..." -ConnectionProviderName "System.Data.SqlClient"

Primjer: Primjena Prve Migracije

🛠️ Kreiranje Baze Podataka
# 1. Kreirajte migraciju
PM> Add-Migration InitialCreate

# 2. Primijenite migraciju (kreira bazu i tabele)
PM> Update-Database

Nakon Update-Database, EF će:

  1. Kreirati bazu podataka (ako ne postoji)
  2. Kreirati tabele na osnovu Model klasa
  3. Kreirati foreign key veze
  4. Kreirati indekse

💡 Provjera Rezultata

Nakon Update-Database, možete provjeriti da li je baza kreirana:

  • Otvorite Server Explorer u Visual Studiju
  • Proširite Data Connections
  • Trebate vidjeti vašu bazu sa tabelama

Primjer: Primjena Dodatne Migracije

🛠️ Dodavanje Nove Kolone
# 1. Promijenite Model klasu (dodajte PhoneNumber)

# 2. Kreirajte migraciju
PM> Add-Migration AddPhoneNumberToEmployee

# 3. Primijenite migraciju
PM> Update-Database

↩️ Rollback Migracija

Ponekad možete htjeti vratiti promjene u bazi. EF omogućava rollback kroz Update-Database -TargetMigration komandu.

🛠️ Rollback Primjer
# Pretpostavimo da imate migracije:
# - InitialCreate
# - AddPhoneNumberToEmployee
# - AddSalaryToEmployee

# Vraćanje na AddPhoneNumberToEmployee (uklanja AddSalaryToEmployee promjene)
PM> Update-Database -TargetMigration AddPhoneNumberToEmployee

# Vraćanje na početak (briše sve tabele)
PM> Update-Database -TargetMigration $InitialDatabase

⚠️ Opasnost Rollback-a

Rollback može obrisati podatke! Ako vratite migraciju koja je dodala kolonu, svi podaci u toj koloni će biti izgubljeni. Uvijek napravite backup prije rollback-a u produkciji.

👥 Timski Rad i Rješavanje Konflikata

Radeći na sistemima u javnoj upravi, često više inženjera radi na istoj bazi na različitim modulima infrastrukture, što dovodi do konflikata u migracijama (npr. dva inženjera naprave i prime različite migracije u isto vrijeme). Ovo se najčešće dešava kada koristite Git za source control.

Rješavanje Konflikata u Migracijama (Best Practice)

Kada povučete kod (git pull) koji sadrži novu migraciju od kolege, a vi ste u međuvremenu lokalno definisali vlastitu na čekanju, Entity Framework će prijaviti konflikt (Model-backing-context has changed) jer hash modeli u __MigrationHistory više nisu isti. Pratite ove korake:

  1. Vratite bazu nazad (Rollback): Izvršite Update-Database -TargetMigration ImeZadnjeZajednickeMigracije (to je migracija prije nego ste vi i vaš kolega počeli raditi). Ovo briše vaše promjene iz lokalne baze.
  2. Uklonite vašu lokalnu migraciju: Obrišite konfliktan .cs fajl vaše migracije i uklonite ga iz projekta. Model klase zadržite!
  3. Preuzmite promjene kolege na bazu: Primijenite kod svog kolege upotrebom Update-Database. Sada vaša baza ima koleginu najnoviju verziju.
  4. Ponovo re-kreirajte: Pozovite Add-Migration ImeVaseMigracije. Ovo generiše novu, čistu migraciju koja sada u sebi kombinuje i prepoznaje promjene vašeg kolege i dodaje vaše izmjene na vrh kao najnoviji sloj.

🌱 Seed Data: Početni i Testni Podaci

Seed data su podaci koji se programski ubacuju u bazu svaki put kada se pokrene Update-Database. Sistemi javne uprave se ne mogu testirati na praznoj bazi. Trebaju vam šifrarnici (katalozi gradova, zanimanja, statusa), defaultni admin račun, i ogromna količina testnih zapisa kako bi klijent mogao vršiti prve vizualizacije portala.

Korištenje Seed() Metode

🛠️ Seed Data - Šifrarnici i Testni Podaci
// Migrations/Configuration.cs
protected override void Seed(JavnaUprava.Models.JavnaUpravaDbContext context)
{
    // 1. Šifrarnici (Lookup data) pomoću AddOrUpdate
    // AddOrUpdate garantuje da nećete duplirati gradove pri višestrukim pokretanjima Seed-a
    context.Departments.AddOrUpdate(
        d => d.DepartmentCode, // Ključ po kojem pretražuje da li već postoji
        new Department { DepartmentName = "Ministarstvo Finansija", DepartmentCode = "MF" },
        new Department { DepartmentName = "Ministarstvo Zdravlja", DepartmentCode = "MZ" },
        new Department { DepartmentName = "Ministarstvo Unutrašnjih Poslova", DepartmentCode = "MUP" }
    );
    
    // Obavezno čuvamo izmjene prije dodavanja djece, kako bi Departments dobili primarne ključeve
    context.SaveChanges();

    // 2. Kreiranje admin korisnika
    context.Employees.AddOrUpdate(
        e => e.JMBG,
        new Employee { FirstName = "Sistem", LastName = "Administrator", JMBG = "0000000000000", DepartmentID = 1 }
    );

    // 3. Generisanje velikih količina testnih podataka samo na Development okruženju
    #if DEBUG
    if (!context.Employees.Any(e => e.FirstName == "TestUser0"))
    {
        for (int i = 0; i < 100; i++)
        {
            context.Employees.Add(new Employee
            {
                FirstName = $"TestUser{i}",
                LastName = $"Prezime{i}",
                JMBG = $"1111111111{i:000}", // Formatiranje broja sa nulama
                DepartmentID = (i % 3) + 1 // Rasporedi u 3 ministarstva
            });
        }
    }
    #endif

    context.SaveChanges();
}

Primjena Seed Data

🛠️ Update-Database sa Seed Data
# Seed metoda se izvršava AUTOMATSKI nakon primjene zadnje migracije!
PM> Update-Database

💡 Moć AddOrUpdate Metode

AddOrUpdate je takozvana "Upsert" komanda. Ona prvo pretražuje bazu na osnovu specifičnog svojstva (npr. DepartmentCode iz primjera iznad). Ako zapis sa tim kodom pronađe (npr "MF"), metoda će izvršiti SQL UPDATE. Ako ga ne pronađe, izvršit će SQL INSERT. Ovo je krucijalno, jer se metoda Seed() okida apsolutno svaki put kada bilo ko pozove Update-Database u Package Manager-u, pa se na ovaj način štitite od kreiranja duplih "Ministarstvo Finansija" rekorda iznova i iznova.

🎯 Praktična Vježba: Kompletna Migracija

Zadatak: Migracija Javne Uprave

Kreirajte i primijenite migracije za vaš JavnaUpravaDbContext.

Zadatak 1: Omogućite migracije i kreirajte InitialCreate koja formira Stats.Departments i Stats.Employees tabele.

Zadatak 2: Dodajte IsActive (bool) u Department model, te kreirajte migraciju AddIsActive.

Zadatak 3: Kreirajte Seed metodu koja puni tabelu sa 3 ministarstva i primijenite je pomoću Update-Database.

💡 Rješenje: Konzola
PM> Enable-Migrations
PM> Add-Migration InitialCreate
PM> Update-Database
PM> Add-Migration AddIsActive
PM> Update-Database

✅ Zaključak

U ovoj lekciji ste naučili:

📚 Sljedeća Lekcija

U Lekciji 2.3 ćemo naučiti kako izvršavati CRUD operacije koristeći Entity Framework. Naučićete LINQ to Entities, Eager/Lazy/Explicit loading, i kako efikasno raditi sa povezanim podacima.