📖 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. Učitavanje Svih Entiteta
// 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()
{
// Učitavanje svih zaposlenika
var employees = db.Employees.ToList();
return View(employees);
}
}
2. Učitavanje Jednog Entiteta 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
// 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
// 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
// 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 isključivo u RAM memoriji servera dok ne
pozovete
SaveChanges().
Ovo ponašanje implementira takozvani Unit of Work obrazac (Jedinica Posla). Šta to znači u praksi za kritične sisteme javne uprave?
- Transakciona obrada: EF obavija sve operacije unutar jedne SQL DB Transakcije
prilkom poziva
SaveChanges()(BEGIN TRAN ... COMMIT). - Sve ili ništa: Ako pokušavate obaviti kompleksnu operaciju - na primjer, dodati novog zaposlenika, automatski mu kreirati prazan zdravstveni karton i upisati log u revizijsku tabelu - a kod zadnjeg koraka baza prijavi grešku, Cijela transakcija se poništava (Rollback). Nijedan zapis se ne upisuje. Sistem nikad ne ostaje u nedosljednom stanju (tzv. "siroče zapisi").
✏️ UPDATE Operacije i State Tracking (Praćenje Stanja)
Ažuriranje Postojećeg Entiteta
// GET: Employees/Edit/5
[HttpGet]
public ActionResult Edit(int id)
{
// Kada koristimo Find() ili FirstOrDefault(), EF počinje pratiti ovaj objekat
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)
{
// Objekat 'employee' je došao sa web forme (HTTP POST), on je "Detached" (EF ne zna za njega).
// Postavljanjem stanja na Modified, nasilno govorimo EF-u da generiše SQL UPDATE za sva njegova polja.
db.Entry(employee).State = System.Data.Entity.EntityState.Modified;
db.SaveChanges(); // SQL UPDATE se šalje u bazu
return RedirectToAction("Index");
}
ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "DepartmentName", employee.DepartmentID);
return View(employee);
}
💡 EntityState i Change Tracker
Entity Framework posjeduje komponentu koja se zove Change Tracker. On non-stop u pozadini prati stanje (EntityState) svakog objekta kojeg je učitao iz baze:
Unchanged- Objekat je tek učitan iz baze (npr. saToList()) i nema izmjena na njegovim propertijima.SaveChanges()ga ignoriše.Added- Pozvali stedb.Employees.Add(NoviObjekat).SaveChanges()će generisati SQL INSERT.Modified- Promijenili ste makar jedan property na objektu koji je učitan iz baze.SaveChanges()će generisati SQL UPDATE samo za izmijenjene kolone, usljed čega je izuzetno optimizovan.Deleted- Pozvali stedb.Employees.Remove(StariObjekat).SaveChanges()će generisati SQL DELETE.Detached- Objekat koji je tek instanciran prekonew Object()ili je došao sa view-a. Nije vezan ni za jedan DbContext.
⏱️ 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]
// 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
// 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.
// 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.
// 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.
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
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext()
: base("DefaultConnection")
{
// Onemogućavanje lazy loading-a
this.Configuration.LazyLoadingEnabled = false;
}
}
🔍 Napredni LINQ Upiti
1. Agregacije
// 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 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. Paginacija (Skip i Take)
Kada u javnim registrima imate desetine hiljada korisnika, nikada ih ne prikazujete na jednom ekranu (to usporava bazu, server i browser korisnika). Potrebno je implementirati Paginaciju (straničenje datagrida).
public ActionResult Index(int page = 1)
{
int pageSize = 10; // Želimo prikazati 10 redova po stranici
// Obavezno prvo SORTIRANJE (OrderBy) pa tek onda Skip, inače SQL baca grešku
var employeesPage = db.Employees
.OrderBy(e => e.LastName)
.Skip((page - 1) * pageSize) // Preskoči zapise sa prethodnih stranica
.Take(pageSize) // Uzmi samo sljedećih 10
.ToList();
// Ovo će na bazi "okinuti" visoko-optimizirani `OFFSET X ROWS FETCH NEXT Y ROWS ONLY` SQL upit.
return View(employeesPage);
}
4. Join Operacije
// 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.
// 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).
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:
- ✅ Kako izvršavati READ operacije sa LINQ to Entities
- ✅ Kako kreirati, ažurirati i brisati entitete (CREATE, UPDATE, DELETE)
- ✅ Tri strategije učitavanja: Eager, Lazy, Explicit
- ✅ Kako izbjegavati N+1 problem
- ✅ Napredne LINQ upite (agregacije, grupisanje, join)
- ✅ Pravilno upravljanje DbContext resursima
📚 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.