Dnes postavíme pomocí LINQ to SQL komponentu model, která bude využívat databázi z minulého dílu.
V MVC frameworku slovo „model“ označuje objekt, který reprezentuje data, která aplikace využívá, a pracuje s nimi. Jak později uvidíte, je komponenta model „srdcem“ MVC aplikace.
ASP.NET MVC podporuje při použití v modelu všechny možné přístupy k datům a je skutečně z čeho vybírat, některé přístupy dost možná znáte sami (jmenuji například LINQ to SQL, který budeme používat my, nebo třeba Entity Framework, obyčejné ADO.NET a mnoho dalších).
Už posté zopakuji, že využijeme LINQ to SQL, pomocí kterého si namapujeme jednotlivé tabulky v databázi na třídy a pomocí nich budeme LINQem získávat data, jako kdybychom komunikovali přímo s databází. Kromě toho si dnes napíšeme tzv. „repository“ třídu (pokud znáte návrhový vzor Repository, tak toto je přesně ono). To znamená, že model aplikace neví, jaká třída se stará o práci s daty, a pokud bychom ji nahradili jinou třídou s jinou funkcionalitou, modelu to bude jedno. Tato nezávislost repository třídy nám umožní snadné provádění unit testů.
LINQ to SQL
LINQ to SQL je ORM (object relational mapper; technika pro převod dat mezi nekompatibilními systémy) dodávaný s .NET Framework 3.5 a výše.
Díky LINQ to SQL můžeme snadno namapovat tabulky v databázi na standardní .NET třídy, kde jednotlivé veřejné vlastnosti odpovídají jednotlivým sloupcům tabulek. Objekty dané třídy pak reprezentují řádky tabulky. My namapujeme tabulky Dinners a RSVP na třídy Dinner a RSVP.
Jediné, co budeme muset udělat, bude přetáhnout tabulky do designéru, odpovídající třídy se nám samy vygenerují, včetně vztahů mezi nimi, a my už pak můžeme vesele psát LINQ dotazy.
Přidání LINQ to SQL Classes do projektu
Začneme kliknutím pravého tlačítka na adresář Models v Solution Exploreru a vybereme možnost Add > New Item. To, co hledáme, se jmenuje LINQ to SQL Classes, proto tuto položku vybereme a nový soubor pojmenujeme NerdDinner.dbml.
Automaticky se otevře designér a můžeme přejít k…
Vytvoření datových tříd s LINQ to SQL
Pro vytvoření tříd reprezentujících tabulky musíme jen dané tabulky přetáhnout ze Server Exploreru do designéru a vše se udělá automaticky.
Všimněte si, že se nově vygenerovaná třída jmenuje Dinner, nikoliv Dinners podle tabulky, došlo tedy ke změně množného čísla na jednotné. Designér toto dělá automaticky, aby byly dodrženy konvence (názvy tříd bývají v singuláru, protože jejich instance reprezentují jednu věc). Pokud by nám jméno nevyhovovalo, mohli bychom ho změnit v Properties anebo poklikáním na hlavičku třídy Dinner v designéru. Toto přejmenování je poměrně zajímavá věc, dokáže identifikovat i jiné tvary množného čísla v angličtině, pokud se například tabulka jmenuje Territories, přejmenuje se na Territory.
Kromě toho ještě designér prozkoumal relace mezi tabulkami a správně identifikoval one-to-many relaci, kterou jsme nastavovali minule. To ukazuje šipka mezi třídami.
Poslední jmenovaná věc nám umožní několik skutečně úžasných věcí. RSVP bude mít vlastnost Dinner, díky které budeme moci přistoupit k večeři asociované s daným člověkem. Zároveň s tím bude mít třída Dinner kolekci „RSVPs“, z/do které budeme moci načítat/ukládat objekty RSVP asociované s danou večeří.
Třída NerdDinnerDataContext
V předchozí kapitole jsme si ukázali, že Visual Studio vytvoří třídy odpovídající jednotlivým tabulkám. Zároveň s tím ale vygeneruje i DataContext třídu, která pro nás bude představovat hlavní cestu pro komunikaci s databází. Protože se naše aplikace jmenuje NerdDinner, bude se DataContext třída jmenovat NerdDinnerDataContext (opět se to udělá automaticky).
Tato nová třída poskytuje dvě vlastnosti: „Dinners“ a „RSVPs“. Proti těmto vlastnostem můžeme psát LINQ dotazy a získávat objekty z databáze. Jak takový dotaz vypadá, uvidíme už vzápětí, protože teď už jdeme psát repository třídu.
Vytvoření třídy DinnerRepository
U malých aplikací nevadí, pokud controller (další z komponenty MVC frameworku; co to je, jsme si řekli v úvodním dílu) přímo komunikuje s DataContext třídou a má přímo v sobě LINQ dotazy. Ovšem jak se postupně aplikace rozrůstá, stane se tento postup složitý na rozšiřování, údržbu a testování.
Jedním ze způsobů, jak se toho vyvarovat, je použití návrhového vzoru Repository, o tom jsme se už bavili na začátku tohoto článku. Přesně to uděláme a za tímto účelem vložíme do adresáře Models novou třídu DinnerRepository (později si nadefinujeme i rozhraní, ale teď bude pro jednoduchost stačit jen tato třída):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace NerdDinner.Models
{
public class DinnerRepository
{
private NerdDinnerDataContext db = new NerdDinnerDataContext();
#region Dotazovací metody
public IQueryable<Dinner> FindAllDinners()
{
return db.Dinners;
}
public IQueryable<Dinner> FindUpcomingDinners()
{
return from dinner in db.Dinners
where dinner.EventDate > DateTime.Now
orderby dinner.EventDate
select dinner;
}
public Dinner GetDinner(int id)
{
return db.Dinners.SingleOrDefault(d => d.DinnerID == id);
}
#endregion
#region Přidat/Odebrat
public void Add(Dinner dinner)
{
db.Dinners.InsertOnSubmit(dinner);
}
public void Delete(Dinner dinner)
{
db.RSVPs.DeleteAllOnSubmit(dinner.RSVPs);
db.Dinners.DeleteOnSubmit(dinner);
}
#endregion
#region Ukládání
public void Save()
{
db.SubmitChanges();
}
#endregion
}
}
Získávání, vkládání, upravování a mazání dat pomocí DinnerRepository
Když už nyní máme napsanou třídu DinnerRepository, pojďme se podívat, co s ní vlastně můžeme dělat.
Získávání dat
Můžeme získat jeden konkrétní Dinner objekt, podle zadaného ID večeře, pomocí metody GetDinner:
DinnerRepository dinnerRepository = new DinnerRepository();
Dinner dinner = dinnerRepository.GetDinner(5);
Tento kód pro změnu vrátí všechny večeře, které se odehrávají někdy v budoucnosti:
DinnerRepository dinnerRepository = new DinnerRepository();
// Vrátí všechny nadcházející večeře.
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
// Vypíše názvy jednotlivých večeří.
foreach (Dinner dinner in upcomingDinners)
{
Response.Write("Název" + dinner.Title);
}
Vkládání a upravování dat
Kód níže přidá do databáze dvě večeře. Žádné změny v databázi nejsou uloženy, dokud explicitně nezavoláme metodu Save. LINQ to SQL balí automaticky všechny změny do databázové transakce, což je označení pro atomickou (nedělitelnou) akci, takže buďto se nakonec uloží všechny změny, nebo žádné, nic mezi tím.
db.SubmitChanges();
DinnerRepository dinnerRepository = new DinnerRepository();
// vytvoření první večeře
Dinner newDinner1 = new Dinner();
newDinner1.Title = "Večeře se Scottem";
newDinner1.HostedBy = "ScotGu";
newDinner1.ContactPhone = "425-703-8072";
// vytvoření druhé večeře
Dinner newDinner2 = new Dinner();
newDinner2.Title = "Akce u Billa";
newDinner2.HostedBy = "BillG";
newDinner2.ContactPhone = "425-555-5151";
// příprava pro přidání večeří
dinnerRepository.Add(newDinner1);
dinnerRepository.Add(newDinner2);
// uložení změn - přidání večeří
dinnerRepository.Save();
Následující kód pro změnu získá jeden Dinner objekt, upraví dvě jeho vlastnosti a pomocí metody Save změny uloží:
DinnerRepository dinnerRepository = new DinnerRepository();
// Podle ID získá večeři.
Dinner dinner = dinnerRepository.GetDinner(5);
// Upraví dvě vlastnosti.
dinner.Title = "Upravený název";
dinner.HostedBy = "Nový pořadatel";
// Uloží změny.
dinnerRepository.Save();
Poslední ukázka v této kapitolce vytáhne z databáze jednu večeři a přiřadí jí RSVP objekt (pro připomenutí, tabulka RSVP obsahuje seznam účastníků večeře). Docílí toho použitím kolekce RSVPs, kterou nám LINQ to SQL automaticky vytvořil na základě vztahu mezi tabulkami. Přidání objektu do kolekce RSVPs se projeví v databázi jako přidání řádku do tabulky RSVP.
DinnerRepository dinnerRepository = new DinnerRepository();
// Získá večeři podle ID.
Dinner dinner = dinnerRepository.GetDinner(5);
// Vytvoří nový RSVP objekt.
RSVP myRSVP = new RSVP();
myRSVP.AttendeeName = "ScottGu";
// Přidá objekt do kolekce RSVPs.
dinner.RSVPs.Add(myRSVP);
// Uloží změny.
dinnerRepository.Save();
Mazání dat
Tady není moc co řešit – kód, který uvidíte níže, vezme jeden Dinner objekt, označí ho pro smazání a zavoláním metody Save ho smaže nadobro.
DinnerRepository dinnerRepository = new DinnerRepository();
// Získá večeři podle ID.
Dinner dinner = dinnerRepository.GetDinner(5);
// Označí večeři pro smazání.
dinnerRepository.Delete(dinner);
// Uloží změny.
dinnerRepository.Save();
Validace
Pokud jste někdy psali aplikaci, která požaduje vstup od uživatele, tak jste se určitě s validacemi už setkali. Jak říká jedno dobré pravidlo: „nikdy nevěř uživateli“, protože se může rozhodnout, že do políčka pro telefonní číslo napíše své jméno nebo do povinného pole pro heslo nenapíše nic. Proto se musíme před použitím/uložením zadaných dat ujistit, že jsou ve správném tvaru. A přesně o tomto bude tato kapitola.
Validace podle schématu
Když nadefinujeme modelové třídy pomocí designéru LINQ to SQL (to je ta pasáž, kde jsme přetahovali tabulky ze Server Exploreru), tak datové typy jednotlivých vlastností korespondují s datovými typy databáze. Například sloupec EventDate je typu datetime, tudíž vlastnost ve vytvořené třídě bude typu DateTime. Díky tomu aplikace vyhodí výjimku, pokud se pokusíme z kódu do tohoto sloupce, respektive vlastnosti, přiřadit například datový typ int nebo bool.
Validace a pravidla business logiky
Validace podle nějakého schématu (třeba podle databáze jako v předchozím odstavci) funguje jako užitečná první linie pro zachycení nesmyslně vyplněných dat, ale je jen málokdy dostatečná. Ve většině reálných scénářů potřebujeme naprogramovat bohatší logiku, časté je například použití regulárních výrazů na ověření správného tvaru telefonního čísla nebo e-mailové adresy. Samozřejmě existuje spousta frameworků a návrhových vzorů, které se zabývají validacemi, my si vystačíme s jedním jednoduchým vzorem, který bude využívat jednu vlastnost (IsValid) a jednu metodu (GetRuleViolations). Vlastnost IsValid bude vracet true nebo false na základě toho, zda jsou všechny validace v pořádku. Pokud nejsou, použijeme metodu GetRuleViolations pro získání chyb.
Přidáme si do projektu jednu třídu, kterou označíme jako „partial“. S tímto klíčovým slovem jste se zcela jistě ve světě .NET už mnohokrát setkali, umožňuje nám rozdělit třídu do více souborů třeba proto, že jednu část třídy generuje samo Visual Studio. Klikneme pravým tlačítkem na adresář Models v Solution Exploreru, zvolíme Add New Item, vybereme typ Class a nový soubor pojmenujeme Dinner.cs. IntelliSense by mělo podtrhnout deklaraci této nové třídy a napovědět vám, abyste přidali klíčové slovo „partial“, protože třída NerdDinner.Models.Dinner už jednou existuje. Nyní by měl váš soubor Dinner.cs vypadat takto:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace NerdDinner.Models
{
public partial class Dinner
{
}
}
Teď už můžeme implementovat jednoduchý „framework“ pro validace, kód si probereme vzápětí:
using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Web;
namespace NerdDinner.Models
{
public partial class Dinner
{
public bool IsValid
{
get { return (GetRuleViolations().Count() == 0); }
}
public IEnumerable<RuleViolation> GetRuleViolations()
{
yield break;
}
partial void OnValidate(ChangeAction action)
{
if (!IsValid)
throw new ApplicationException("Uložení se nepodařilo kvůli nedodržení business pravidel");
}
}
public class RuleViolation
{
public string ErrorMessage { get; private set; }
public string PropertyName { get; private set; }
public RuleViolation(string errorMessage, string propertyName)
{
ErrorMessage = errorMessage;
PropertyName = propertyName;
}
}
}
Teď těch slíbených pár poznámek ke kódu.
- V deklaraci třídy Dinner je klíčové slovo „partial“, takže kód v ní bude zkombinován s automaticky vygenerovaným kódem, který udělal LINQ to SQL designér, a zkompilován do jedné třídy.
- RuleViolation je pomocná třída, která nám umožní poskytovat další informace o porušení pravidel.
- Metoda Dinner.GetRuleViolations bude sloužit pro validování a pak vrátí kolekci typu IEnumerable, přes kterou můžeme získat více informací o případných chybách.
- Vlastnost Dinner.IsValid poskytuje informaci o tom, jestli má Dinner objekt nějaké RuleViolation objekty.
- Metoda Dinner.OnValidate je opět definována jako partial, tudíž má signaturu nadefinovanou jinde, než je její implementace. Je volána vždy, když se Dinner objekt chystá k uložení do databáze. Naše implementace této metody zajistí, že večeře nebude do databáze uložena, pokud došlo k nějakým chybám ve validaci.
Pojďme přidat do metody GetRuleViolations pár pravidel, abychom vůbec měli podle čeho kontrolovat správnost dat:
public IEnumerable<RuleViolation> GetRuleViolations()
{
if (String.IsNullOrEmpty(Title))
yield return new RuleViolation("Název je povinný", "Title");
if (String.IsNullOrEmpty(Description))
yield return new RuleViolation("Popis je povinný", "Description");
if (String.IsNullOrEmpty(HostedBy))
yield return new RuleViolation("Pořadatel je povinný", "HostedBy");
if (String.IsNullOrEmpty(Address))
yield return new RuleViolation("Adresa je povinná", "Address");
if (String.IsNullOrEmpty(Country))
yield return new RuleViolation("Země je povinná", "Country");
if (String.IsNullOrEmpty(ContactPhone))
yield return new RuleViolation("Telefon je povinný", "ContactPhone");
if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
yield return new RuleViolation("Telefon se neshoduje se zemí", "ContactPhone");
yield break;
}
Klíčové slovo „yield“ se moc často nevidí, ale v některých situacích je neskutečně užitečné, nicméně je to jen syntaktický cukr, který byl uveden v C# 2.0. Normální „return“ vrátí z metody hodnotu a ukončí její průběh, zatímco „yield return“ také vrátí hodnotu, ale metoda dál pokračuje. Můžeme vrátit i další hodnoty, všechny se totiž postupně přidávají do IEnumerable kolekce (jdou použít i jiné typy, ale samozřejmě musí být naše metoda definovaná tak, aby vracela typ IEnumerable, IEnumerator nebo jiné).
Prvních šest pravidel jen požaduje, aby jejich vlastnosti nebyly prázdné (kontrolujeme to pomocí metody String.IsNullOrEmpty). Sedmé pravidlo je asi nejzajímavější, volá metodu IsValidNumber ze třídy PhoneValidator (napíšeme si ji za chvíli), která bude pomocí regulárních výrazů kontrolovat to, zda formát telefonního čísla odpovídá vybranému státu. Přidejme proto do adresáře model novou třídu pojmenovanou PhoneValidator, která bude mít následující zdrojový kód:
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace NerdDinner.Models
{
public class PhoneValidator
{
private static IDictionary<string, Regex> countryRegex = new Dictionary<string, Regex>()
{
{
"USA",
new Regex("^[2-9]\d{2}-\d{3}-\d{4}$")
},
{
"UK",
new Regex(
"(^1300\d{6}$)|(^1800|1900|1902\d{6}$)|(^0[2|3|7|8]{1}[0-9]{8}$)|(^13\d{4}$)|(^04\d{2,3}\d{6}$)")
},
{
"Česká republika",
new Regex(
"^(\+420)? ?[0-9]{3} ?[0-9]{3} ?[0-9]{3}$")
},
};
public static bool IsValidNumber(string phoneNumber, string country)
{
if (country != null && countryRegex.ContainsKey(country))
return countryRegex[country].IsMatch(phoneNumber);
else
return false;
}
public static IEnumerable<string> Countries
{
get
{
return countryRegex.Keys;
}
}
}
}
Naše aplikace bude umět kontrolovat správnost amerických, anglických a českých telefonních čísel. Pokud by byla potřeba, není problém najít na internetu už hotové regulární výrazy pro formáty čísel ostatních národností.
Zachytávání výjimek a porušení business pravidel
Od teď už můžeme vytvářet a upravovat večeře a zjistit, zda je daný objekt validní i bez toho, aby aplikace vyvolávala různé výjimky:
Dinner dinner = dinnerRepository.GetDinner(5);
dinner.Country = "Česká republika";
dinner.ContactPhone = "+420 602 345 279x";
if (!dinner.IsValid) {
var errors = dinner.GetRuleViolations();
// Zde uděláme něco pro opravení chyb.
}
Když se pokusíme uložit večeře v chybném „formátu“, bude vyvolána výjimka při zavolání Save metody. To je díky tomu, že LINQ to SQL automaticky volá metodu Dinner.OnValidate (ta, co jsme ji definovali jako „partial“) a ta objeví případné chyby. Tuto výjimku můžeme zachytit a udělat, co budeme potřebovat:
Dinner dinner = dinnerRepository.GetDinner(5);
try
{
dinner.Country = "Česká republika";
dinner.ContactPhone = "blabla";
dinnerRepository.Save();
}
catch
{
var errors = dinner.GetRuleViolations();
// Zde uděláme něco pro opravení chyb.
}
Podařilo se nám vytvořit flexibilní validační mini-framework, naučili jsme se pracovat s klíčovým slovem „yield“, s návrhovým vzorem Repository a v neposlední řadě i s LINQ to SQL. V příštím díle se podíváme na zoubek posledních dvěma MVC komponentám: controller a view.