Wprowadzenie
W artykule tym zostanie omówione podejście Code First. Zobaczymy również jakie są korzyści zastosowania takiego podejścia.
Zanim jednak przejdziemy do właściwej treści artykułu należy się skupić na nowoczesnym podejściu do projektowania aplikacji. Tradycyjne podejście do projektowania i tworzenia aplikacji to tzw. Data Driven Design. Oznacza to, iż w pierwszej kolejności myślimy o wymaganych danych niezbędnych do wypełnienia obiektu biznesowego a następnie przystępowaliśmy do tworzenia aplikacji na podstawie istniejącego schematu bazy danych. Podejście takie jest wciąż bardzo popularne i często używane. W takim wypadku powinniśmy używać podejścia Database First.
Alternatywnym podejściem jest Domain Driven Design. W podejściu tym myślimy o encjach i modelach, których potrzebujemy być rozwiązać pewien problem związany z obiektem biznesowym. Jeżeli potrzebujemy przechowywać aktualny stan naszych modeli możemy tego dokonać poprzez umieszczenie tych modeli w bazie danych. Innymi słowy, w pierwszej kolejności tworzymy model w naszej aplikacji a w kolejnym kroku możemy napisać logikę odpowiedzialną za wykonywanie operacji na tym modelu, np. zapisywanie wyników do relacyjnej bazy danych. Entity Framework Code First pozwala na tworzenie modeli aplikacji przy użyciu podejścia Domain Driven Design, które następnie mogą zostać zapisane w bazie danych.
EF pozwala nam na pisanie obiektów POCO (Plain Old CLR Object) dla naszych modeli a następnie na przechowywanie ich w bazie danych za pomocą definicji klasy DbContext. Poniżej kilka z korzyści płynących z zastosowania takiego podejścia:
-
możliwość wsparcia projektowego Domain Driven Design;
- możliwość szybszego rozpoczęcia procesu tworzenia kodu (bez konieczności czekania na gotową bazę danych);
- klasy modeli są dużo czytelniejsze ponieważ nie zawierają logiki związanej z przechowywaniem modeli w bazie danych;
- warstwa przechowywania danych może być zmieniona bez żadnego wpływu na istniejące modele.
Czym w takim wypadku są obiekty POCO? Są to proste obiekty, które nie zawierają żadnej logiki związanej z dostępem do danych ale wciąż dostarczają możliwości takie jak lazy loading.
Adnotacja:
Artykuł ten jest napisany z perspektywy osoby zupełnie początkującej, która stawia pierwsze kroki w takim podejściu projektowym. Zawiera jedynie podstawowe informacje oraz kod pozwalający na rozpoczęcie pracy zgodnej z takim podejściem.
Aplikacja
Spróbujemy zrozumieć podejście Code First na przykładzie małej aplikacji webowej napisanej w technologii ASP.NET MVC. Aplikacja ta będzie pozwalała nam na:
- Wyświetlenie listy wszystkich samochodów
- Wyświetlenie szczegółów wybranego samochodu
- Dodawanie nowych samochodów
- Usuwanie samochodów
- Edycje wcześniej zdefiniowanych samochodów
- Dodawanie własnych komentarzy dotyczących wybranego samochodu
Patrząc na powyższe punkty potrzebujemy dwóch modeli. Pierwszy dla samochodów a drugi dla komentarzy.
Tworzenie POCO’s
Zacznijmy od utworzenia prostych modeli:
public class CarModel
{
public int CarId { get; set; }
public string Brand { get; set; }
public string Model { get; set; }
}
public class CommentModel
{
public int Id { get; set; }
public int CarId { get; set; }
public string Comment { get; set; }
}
Zdefiniowaliśmy niezdbędne właściwości. Pozostało nam jeszcze kilka rzeczy do zrobienia. Pomiędzy tymi modelami istnieje relacja jeden do wielu. Musimy uwzględnić to w naszym projekcie. Ponadto, musimy poinformować moduł tworzenia bazy danych o nazwach tabel, kolumnach itd. Możemy tego dokonać w w klasach naszych modeli. Po wprowdzeniu tych zmian do naszych klas będą one wyglądały w następujący sposób:
[Table("Cars")] // za pomocą atrybutu nadajemy nazwę naszej tabeli
public class CarModel
{
[Key] // ustawiamy klucz główny tabeli
public int CarId { get; set; }
public string Brand { get; set; }
public string Model { get; set; }
// do każdego samochodu możemy przypisać wiele komentarzy
public virtual ICollection<CommentModel> Comments { get; set; }
}
[Table("Comments")] // za pomocą atrybutu nadajemy nazwę naszej tabeli
public class CommentModel
{
[Key] // ustawiamy klucz główny tabeli
public int Id { get; set; }
[ForeignKey("Cars")] // ustawiamy klucz obcy
public int CarId { get; set; }
public string Comment { get; set; }
// będziemy widzieli, które samochody przynależą do tego komentarza
public virtual CarModel Cars { get; set; }
}
Tworzenie klasy Context
W poprzednim kroku przygotowaliśmy nasze modele. Przejdźmy teraz do utworzenia tytułowej klasy. Dokonamy tego automatycznie: Klikamy na nasz projekt -> Add -> New Item -> Data -> ADO.NET Entity Data Model -> wybieramy: Empty Code First Model. Została utworzona klasa CarDbContext - nazwa nadana w kreatorze. Automatycznie wygenenerowana klasa wygląda tak:
public class CarDbContext : DbContext
{
// Your context has been configured to use a 'CarDbContext' connection string from your application's
// configuration file (App.config or Web.config). By default, this connection string targets the
// 'EF_CodeFirstApproach.CarDbContext' database on your LocalDb instance.
//
// If you wish to target a different database and/or database provider, modify the 'CarDbContext'
// connection string in the application configuration file.
public CarDbContext()
: base("name=CarDbContext")
{
}
// Add a DbSet for each entity type that you want to include in your model. For more information
// on configuring and using a Code First model, see http://go.microsoft.com/fwlink/?LinkId=390109.
// public virtual DbSet MyEntities { get; set; }
}
Jeden z komentarzy mówi, aby dodać obiekt DbSet dla każdej encji, którą chcemy umieścić w naszym modelu. Pozwoli nam to na wykonywanie wszystkich operacji CRUD na naszych modelach. Po zmianach nasza klasa przedstawia się w poniższy sposób:
public class CarDbContext : DbContext
{
public CarDbContext()
: base("name=CarDbContext")
{
}
public DbSet<CarModel> Cars { get; set; }
public DbSet<CommentModel> Comments { get; set; }
}
Ustawienia bazy danych i lokalizacji
Klasa CarDbContext została automatycznie dodana do naszego projektu. Dodatkowo do pliku web.config został dodany connectionString:
<add name="CarDbContext" connectionString="data source=(LocalDb)\MSSQLLocalDB;initial catalog=EF_CodeFirstApproach.CarDbContext;
integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
Ważną rzeczą do zapamiętania jest fakt, że nazwa naszego połączenia jest taka sama jak klasa DbContext, która została utworzona. Jeżeli zachowamy nazwę połączenia taką samą jak nazwa klasy, będzie ona wiedziała, którego połączenia użyć do zachowania danych w bazie danych. Wpis ten oczywiście jest edytowalny więc możemy dać dowolną nazwę dla naszego połączenia, np. jeżeli chcemy nadać inną nazwę dla naszego połącznia lub chcemy użyć zdefiniowanego wcześniej połączenia musimy jedynie przekazać nazwę naszego połączenia do konstruktora naszej klasy DbContext.
Dla lepszego zapoznania się z connectionString zmienimy to połączenie tak, aby wskazywało do przykładowej bazy danych na pulpicie:
<add name="CarDbContext" connectionString="data source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=C:\Users\PawelL\Desktop\sampleDb.mdf;
integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
Testowanie modeli oraz kontekstu
Do przetestowania powyższych definicji niezbędne będzie utworzenie prostego kontrolera, który wykonana dla nas operacje CRUD dla encji Car.
Klikamy na folder Controllers w naszym projekcie -> Add -> Controller -> MVC 5 Contoller with views, using Entity Framework -> Wybieramy utworzony wcześniej model CarModel oraz automatycznie utworzony kontekst CarDbContext i klikamy przycisk Add. Został utworzony kontroler pozwalający na dokonywanie operacji CRUD na encji CarModel:
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web.Mvc;
using EF_CodeFirstApproach.Models;
namespace EF_CodeFirstApproach.Controllers
{
public class CarsController : Controller
{
private CarDbContext db = new CarDbContext();
// GET: CarModels
public ActionResult Index()
{
return View(db.Cars.ToList());
}
// GET: CarModels/Details/5
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
CarModel carModel = db.Cars.Find(id);
if (carModel == null)
{
return HttpNotFound();
}
return View(carModel);
}
// GET: CarModels/Create
public ActionResult Create()
{
return View();
}
// POST: CarModels/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "CarId,Brand,Model")] CarModel carModel)
{
if (ModelState.IsValid)
{
db.Cars.Add(carModel);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(carModel);
}
// GET: CarModels/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
CarModel carModel = db.Cars.Find(id);
if (carModel == null)
{
return HttpNotFound();
}
return View(carModel);
}
// POST: CarModels/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "CarId,Brand,Model")] CarModel carModel)
{
if (ModelState.IsValid)
{
db.Entry(carModel).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(carModel);
}
// GET: CarModels/Delete/5
public ActionResult Delete(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
CarModel carModel = db.Cars.Find(id);
if (carModel == null)
{
return HttpNotFound();
}
return View(carModel);
}
// POST: CarModels/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
CarModel carModel = db.Cars.Find(id);
db.Cars.Remove(carModel);
db.SaveChanges();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
}
}
Dodawanie widoków
Widoki zostały przygotowane automatycznie w momencie, gdy dodawaliśmy nowy kontroler. Co należałoby zrobić jeżeli chcielibyśmy dodać nowy widok?
Przygotowaniem widoków zajmuje się dostępny kreator ASP.NET MVC. W ramach tego podpunktu przygotujemy jeden widok dla metody Index() (tylko jako praktyka – widok ten jest już utworzony).
Przechodzimy do naszego kontolera, do metody Index(). Prawym przyciskiem myszy klikamy na zwracany przez nas widok i wybieramy: Add View:
W oknie konfiguracji wybieramy następujące opcje:
Jest to widok przedstawiający listę wszystkich elementów do wyświetlenia. Gdybyśmy chcieli np. utworzyć widok kasowania danych jako wybrany Template wskazalibyśmy Delete.
Testowanie aplikacji
Wszystko zostało już przygotowane. Kolejnym krokiem jest przetestowanie działania naszej aplikacji. W momencie piewszego uruchomienia naszej aplikacji nie zostaną wyświetlone żadne dane – nie zostały jeszcze zdefiniowane. Utworzona zostanie za to baza danych w lokalizacji wskazanej w pliku web.config.
Dodajmy zatem kilka elementów do naszej listy:
Tak prezentuje się teraz nasza lista:
W kolejnym kroku możemy wyświetlić interesujące nas dane szczegółowe:
Zarządzanie relacjami – dodawanie komentarzy
Jak mogliście zobaczyć na powyższym zrzucie ekranu nie ma żadnych informacji dotyczących komentarzy odnośnie wskazanego samochodu. W pierwszej kolejności musimy zmodyfikować widok naszego kontolera Cars, tj. Details. Poniżej kod naszego widoku po wprowadzonych zmianach:
@model EF_CodeFirstApproach.Models.CarModel
@{
ViewBag.Title = "Szczegóły";
}
<h2>Szczegóły</h2>
<div style="border:1px solid rgb(128, 128, 128);padding:15px">
<dl class="dl-horizontal">
<dt>
@Html.DisplayName("Marka: ")
</dt>
<dd>
@Html.DisplayFor(model => model.Brand)
</dd>
<dt>
@Html.DisplayName("Model: ")
</dt>
<dd>
@Html.DisplayFor(model => model.Model)
</dd>
</dl>
</div>
<div style="border:1px solid rgb(128, 128, 128);padding:15px;margin-top:15px">
<h4>Komentarze</h4>
@if (Model.Comments.Count == 0)
{
<span>Brak komentarzy</span>
}
else
{
foreach (var item in Model.Comments)
{
<p>@item.Comment</p>
}
}
<br />
@* Link wskazuje na kontoler odpowiedzialny za dodawanie komentarzy do wskazanego samochodu *@
@* Przekazujemy CarId, aby wiedzieć dokładnie dla którego samochodu dodajemy komentarz *@
@Html.ActionLink("Dodaj nowy komentarz", "Create", "Comments", new { id = Model.CarId }, null)
</div>
<br />
<br />
<p>
@Html.ActionLink("Edycja", "Edit", new { id = Model.CarId }) |
@Html.ActionLink("Powrót do listy", "Index")
</p>
A tak prezentuje się nasz widok po wprowadzeniu powyższych zmian:
Nie możemy zapominać o przygotowaniu odpowiedniej metody do której przechodzimy po wciśnięciu przycisku "Dodaj nowy komentarz". Tworzymy kontroler dla naszych komentarzy a następnie, w piewszej kolejności, dodajemy metodę Create(…):
// Jako parametr przekazujemy CarId
public ActionResult Create(int id)
{
// Pobieramz dane dotyczące wskazanego samochodu
CarModel car = db.Cars.Single(a => a.CarId == id);
// Tworzymy nowy obiekt komentarz
CommentModel comm = new CommentModel();
// Wypełniamy obiekt niezbędnymi informacjami
comm.CarId = id;
comm.Cars.Brand = car.Brand;
comm.Cars.Model = car.Model;
return View(comm);
}
Widok dla danej metody:
Oraz kod tego widoku:
@model EF_CodeFirstApproach.Models.CommentModel
@{
ViewBag.Title = "Dodaj komentarz";
}
<h2>Dodaj komentarz</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.HiddenFor(model => model.Id)
@Html.HiddenFor(model => model.CarId, new { htmlAttributes = new { @class = "form-control" } })
<div class="form-group">
@Html.Label("Marka i model samochodu:", htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@{
string car = Model.Cars.Brand + " " + Model.Cars.Model;
}
@Html.Label(car, new { htmlAttributes = new { @class = "form-control" } })
</div>
</div>
<div class="form-group">
@Html.Label("Komentarz", htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(model => model.Comment, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Comment, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Zapisz" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Powrót to listy", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Podczas tworzenia kontolera zostanie automatycznie utworzona metoda pozwalająca na zapisywanie komentarzy:
// POST: CommentModels/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,CarId,Comment")] CommentModel commentModel)
{
if (ModelState.IsValid)
{
db.Comments.Add(commentModel);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CarId = new SelectList(db.Cars, "CarId", "Brand", commentModel.CarId);
return View(commentModel);
}
A tak prezentuje się szczegółowy widok po dodaniu kilku komentarzy: