MODUL 2 - LEKCIJA 3

CRUD Operacije i LINQ to Entities

Izvršavanje Create, Read, Update, Delete operacija i rad sa povezanim podacima

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

📖 Uvod u CRUD Operacije

CRUD (Create, Read, Update, Delete) su osnovne operacije koje svaka aplikacija mora izvršavati nad podacima. Entity Framework omogućava vam da izvršavate ove operacije koristeći LINQ to Entities - LINQ sintaksu koja se prevodi u SQL upite.

🔑 LINQ to Entities

LINQ (Language Integrated Query) omogućava vam da pišete upite koristeći C# sintaksu. Entity Framework prevodi LINQ upite u SQL i izvršava ih na bazi podataka. Ovo daje type safety, IntelliSense podršku, i kompajlersku provjeru grešaka.

📖 READ Operacije: Čitanje Podataka

1. Dohvatanje Svih Entiteta

🛠️ Dohvatanje Svih Zaposlenika
// Controllers/EmployeesController.cs
using System.Linq;
using System.Web.Mvc;
using JavnaUprava.Models;

public class EmployeesController : Controller
{
    private JavnaUpravaDbContext db = new JavnaUpravaDbContext();

    public ActionResult Index()
    {
        // Dohvatanje svih zaposlenika
        var employees = db.Employees.ToList();
        return View(employees);
    }
}

2. Dohvatanje Jednog Entiteta po ID-u

🛠️ Dohvatanje po ID-u
public ActionResult Details(int id)
{
    // Find() metoda traži po primarnom ključu
    var employee = db.Employees.Find(id);
    
    if (employee == null)
    {
        return HttpNotFound();
    }
    
    return View(employee);
}

// Alternativno, koristeći LINQ:
public ActionResult Details(int id)
{
    var employee = db.Employees.FirstOrDefault(e => e.Id == id);
    
    if (employee == null)
    {
        return HttpNotFound();
    }
    
    return View(employee);
}

3. Filtriranje i Pretraživanje

🛠️ LINQ Where Klauzula
// Filtriranje zaposlenika po ID-u ministarstva
public ActionResult ByDepartment(int departmentId)
{
    var employees = db.Employees
        .Where(e => e.DepartmentID == departmentId)
        .ToList();
    
    return View(employees);
}

// Pretraživanje po JMBG-u ili imenu
public ActionResult Search(string searchTerm)
{
    var employees = db.Employees
        .Where(e => e.JMBG.Contains(searchTerm) || 
                   e.FirstName.Contains(searchTerm) || 
                   e.LastName.Contains(searchTerm))
        .ToList();
    
    return View(employees);
}

// Napredna pretraga: Zaposlenici u ministarstvu sa prezimenom na 'A'
public ActionResult FilterInDepartment(int deptId)
{
    var employees = db.Employees
        .Where(e => e.DepartmentID == deptId && 
                   e.LastName.StartsWith("A"))
        .OrderBy(e => e.LastName)
        .ToList();
    
    return View(employees);
}

4. Sortiranje

🛠️ OrderBy i OrderByDescending
// Sortiranje po prezimenu (rastuće)
var employees = db.Employees
    .OrderBy(e => e.LastName)
    .ToList();

// Sortiranje po datumu zaposlenja (opadajuće)
var employees = db.Employees
    .OrderByDescending(e => e.HireDate)
    .ToList();

// Višestruko sortiranje
var employees = db.Employees
    .OrderBy(e => e.DepartmentId)
    .ThenBy(e => e.LastName)
    .ToList();

➕ CREATE Operacije: Dodavanje Podataka

Dodavanje Novog Entiteta

🛠️ Create Action Metode
// GET: Employees/Create
[HttpGet]
public ActionResult Create()
{
    // Priprema liste ministarstava za dropdown
    ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "DepartmentName");
    return View();
}

// POST: Employees/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Employee employee)
{
    if (ModelState.IsValid)
    {
        // Validacija JMBG-a (primjer poslovne logike)
        if (employee.JMBG.Length != 13) {
            ModelState.AddModelError("JMBG", "JMBG mora imati 13 cifara");
            ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "DepartmentName", employee.DepartmentID);
            return View(employee);
        }

        db.Employees.Add(employee);
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    
    ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "DepartmentName", employee.DepartmentID);
    return View(employee);
}

🔑 SaveChanges() Metoda i Unit of Work Šablon

SaveChanges() je ključna metoda koja stvarno šalje promjene u bazu podataka. Sve promjene (Add, Update, Delete) se čuvaju u memoriji dok ne pozovete SaveChanges().

Ovo ponašanje implementira takozvani Unit of Work obrazac. Šta to znači u praksi za sisteme državne uprave?

  • Transakciona obrada: EF obavija sve operacije unutar jedne SQL transakcije prilkom poziva SaveChanges().
  • Sve ili ništa: Ako pokušavate dodati 50 novih zaposlenika, a kod 49. dođe do greške validacije baze (npr. isti JMBG), Cijela transakcija se poništava i nijedan radnik se ne upisuje. Sistem nikad ne ostaje u nedosljednom stanju.

✏️ UPDATE Operacije: Ažuriranje Podataka

Ažuriranje Postojećeg Entiteta

🛠️ Edit Action Metode
// GET: Employees/Edit/5
[HttpGet]
public ActionResult Edit(int id)
{
    var employee = db.Employees.Find(id);
    
    if (employee == null)
    {
        return HttpNotFound();
    }
    
    ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "DepartmentName", employee.DepartmentID);
    return View(employee);
}

// POST: Employees/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Employee employee)
{
    if (ModelState.IsValid)
    {
        db.Entry(employee).State = System.Data.Entity.EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    
    ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "DepartmentName", employee.DepartmentID);
    return View(employee);
}

💡 EntityState

Entity Framework prati stanje svakog entiteta:

  • Added - novi entitet
  • Modified - entitet je promijenjen
  • Deleted - entitet je obrisan
  • Unchanged - entitet nije promijenjen

⏱️ Rješavanje Konflikata u Višekorisničkom Okruženju (Concurrency)

U sistemima javne uprave česta je situacija da dva referenta istovremeno otvore isti zapis (npr. profil zaposlenika). Ako oboje naprave izmjene i kliknu "Spasi", čije se izmjene primjenjuju?

Entity Framework podrazumijevano koristi princip Pobjeđuje Onaj Koji Posljednji Sačuva (Last In Wins). Međutim, to dovodi do tihog gubitka podataka prvog referenta. Da bismo ovo spriječili, implementiramo Optimistično Zaključavanje (Optimistic Concurrency).

Implementacija uz pomoć atributa [Timestamp]

🛠️ Optimistično Zaključavanje
// 1. Dodavanje RowVersion kolone u Model
public class Employee 
{
    public int EmployeeID { get; set; }
    public string FirstName { get; set; }
    
    // Ovo polje MS SQL Server automatski mijenja pri svakom Update-u objekta (bilo koje kolone)
    [Timestamp]
    public byte[] RowVersion { get; set; } 
}

// 2. Tretiranje greške u Controlleru
[HttpPost]
public ActionResult Edit(Employee employee)
{
    try 
    {
        db.Entry(employee).State = EntityState.Modified;
        db.SaveChanges(); // Pokušaj snimanja
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException ex) 
    {
        // Okida se ako je neko drugi u međuvremenu promijenio zapis u bazi!
        ModelState.AddModelError(string.Empty, "Zapis je izmijenjen od strane drugog korisnika nakon što ste ga učitali. Molimo osvježite stranicu, pregledajte tuđe izmjene i pokušajte ponovo.");
        return View(employee);
    }
}

💡 Kako funkcionira [Timestamp]

EF će na osnovu [Timestamp] prilikom generisanja SQL Update skripte unutar WHERE klauzule uvijek provjeriti da li je RowVersion u bazi jednak onom starom koji ste vi povukli na formu. Ako nije jednak, znači da je kolega obavio UPDATE u sekundi kada ste vi ukucavali vaše tekstove, i baza automatski odbija vašu operaciju i sprečava gubitak koda.

🗑️ DELETE Operacije: Brisanje Podataka

🛠️ Delete Action Metode
// GET: Employee/Delete/5
[HttpGet]
public ActionResult Delete(int id)
{
    var employee = db.Employees.Find(id);
    
    if (employee == null)
    {
        return HttpNotFound();
    }
    
    return View(employee);
}

// POST: Employee/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
    var employee = db.Employees.Find(id);
    
    if (employee != null)
    {
        // Brisanje entiteta
        db.Employees.Remove(employee);
        
        // Spremanje promjena
        db.SaveChanges();
    }
    
    return RedirectToAction("Index");
}

⚠️ Cascade Delete

Ako imate foreign key veze sa cascadeDelete: true, brisanje roditeljskog entiteta će automatski obrisati i povezane entitete. Budite oprezni!

🔄 Strategije Učitavanja: Eager, Lazy, Explicit

Entity Framework podržava tri strategije za učitavanje povezanih podataka (navigation properties):

1. Eager Loading - Učitavanje Odmah

Eager Loading učitava povezane entitete odmah u istom upitu koristeći Include() metodu.

🛠️ Eager Loading sa Include()
// Učitavanje zaposlenika sa njihovim ministarstvima
var employees = db.Employees
    .Include(e => e.Department)
    .ToList();

// Filtriranje pa učitavanje
var mfEmployees = db.Employees
    .Where(e => e.Department.DepartmentCode == "MF")
    .Include(e => e.Department)
    .ToList();

💡 Kada Koristiti Eager Loading?

  • Kada znaте da će vam trebati povezani podaci
  • Kada želite smanjiti broj upita ka bazi
  • Kada radite sa malim brojem povezanih entiteta

2. Lazy Loading - Učitavanje na Zahtjev

Lazy Loading automatski učitava povezane entitete kada im pristupite. Ovo je default ponašanje u EF 6.

🛠️ Lazy Loading Primjer
// Učitavanje zaposlenika (bez Department-a)
var employee = db.Employees.Find(1);

// Department se učitava automatski kada mu pristupite
var departmentName = employee.Department.Name; // EF izvršava dodatni upit ovdje

// Ovo može rezultovati N+1 problemom!
foreach (var emp in db.Employees)
{
    // Svaki pristup Department-u izvršava novi upit
    Console.WriteLine(emp.Department.Name); // N upita!
}

⚠️ N+1 Problem

N+1 problem se dešava kada imate 1 upit za listu entiteta, a zatim N dodatnih upita za povezane entitete. Ovo može značajno usporiti aplikaciju!

Rješenje: Koristite Include() za Eager Loading.

3. Explicit Loading - Eksplicitno Učitavanje

Explicit Loading omogućava vam da eksplicitno učitavate povezane entitete kada vam trebaju, koristeći Load() metodu.

🛠️ Explicit Loading
var employee = db.Employees.Find(1);

// Eksplicitno učitavanje Department-a
db.Entry(employee)
    .Reference(e => e.Department)
    .Load();

// Sada možete pristupiti Department-u bez dodatnog upita
var departmentName = employee.Department.Name;

// Eksplicitno učitavanje kolekcije
db.Entry(employee)
    .Collection(e => e.Projects)
    .Load();

Onemogućavanje Lazy Loading

🛠️ Isključivanje Lazy Loading-a
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext() 
        : base("DefaultConnection")
    {
        // Onemogućavanje lazy loading-a
        this.Configuration.LazyLoadingEnabled = false;
    }
}

🔍 Napredni LINQ Upiti

1. Agregacije

🛠️ Count, Sum, Average, Max, Min
// Broj zaposlenih u MF
var count = db.Employees.Count(e => e.Department.DepartmentCode == "MF");

// Ukupan budžet svih ministarstava
var totalBudget = db.Departments.Sum(d => d.Budget);

// Najveći budžet
var maxBudget = db.Departments.Max(d => d.Budget);

// Grupisanje zaposlenih po ministarstvu
var employeesByDept = db.Employees
    .GroupBy(e => e.DepartmentID)
    .Select(g => new { 
        DeptId = g.Key, 
        Count = g.Count() 
    })
    .ToList();

2. GroupBy

🛠️ Grupisanje Podataka
// Grupisanje zaposlenika po odjelu
var employeesByDept = db.Employees
    .GroupBy(e => e.DepartmentId)
    .Select(g => new {
        DepartmentId = g.Key,
        Employees = g.ToList(),
        Count = g.Count()
    })
    .ToList();

3. Join Operacije

🛠️ LINQ Join
// Eksplicitni JOIN (rijetko potreban - koristite navigation properties)
var result = from emp in db.Employees
             join dept in db.Departments on emp.DepartmentId equals dept.Id
             select new {
                 EmployeeName = emp.FirstName + " " + emp.LastName,
                 DepartmentName = dept.Name
             };

// Bolje: koristite navigation properties
var result = db.Employees
    .Select(e => new {
        EmployeeName = e.FirstName + " " + e.LastName,
        DepartmentName = e.Department.Name
    })
    .ToList();

🧹 Disposing DbContext

Važno je pravilno zatvoriti DbContext kako biste oslobodili resurse. Najbolji način je korištenje using statement-a.

🛠️ Pravilno Disposing
// DOBRO: Korištenje using statement-a
public ActionResult Index()
{
    using (var db = new ApplicationDbContext())
    {
        var employees = db.Employees.ToList();
        return View(employees);
    } // DbContext se automatski zatvara ovdje
}

// ILI: Implementacija IDisposable u Controller-u
public class EmployeeController : Controller
{
    private ApplicationDbContext db = new ApplicationDbContext();

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            db.Dispose();
        }
        base.Dispose(disposing);
    }
}

🎯 Praktična Vježba: Kompletan CRUD

Zadatak: Implementirajte CRUD za Ministarstva

Kreirajte kompletan CRUD (Create, Read, Update, Delete) za entitet Department.

Zadatak 1: Kreirajte DepartmentsController sa Index akcijom koja prikazuje listu ministarstava sa brojem zaposlenih (koristite GroupBy ili Include).

Zadatak 2: Implementirajte Create akciju za dodavanje novog ministarstva.

Zadatak 3: Implementirajte Delete akciju koja sprečava brisanje ministarstva ako ima zaposlenih (Cascade Delete protection).

💡 Rješenje: Index sa Eager Loading
public ActionResult Index()
{
    // Učitavamo ministarstva i brojimo zaposlene
    var depts = db.Departments
        .Include(d => d.Employees)
        .OrderByDescending(d => d.Budget)
        .ToList();
    
    return View(depts);
}

✅ Zaključak

U ovoj lekciji ste naučili:

📚 Sljedeći Modul

U Modulu 3 ćemo naučiti kako koristiti Controller-e i Action metode za obradu korisničkih zahtjeva. Naučićete Model Binding, ViewData, ViewBag, TempData, i Action Filtere.