Zpracování souběžnosti s Entity Frameworkem v aplikaci ASP.NET MVC (7 z 10)
Tom Dykstra
Ukázková webová aplikace Contoso University ukazuje, jak vytvářet aplikace ASP.NET MVC 4 pomocí entity Framework 5 Code First a sady Visual Studio 2012. Informace o sérii kurzů najdete v prvním kurzu série.
Poznámka:
Pokud narazíte na problém, který nemůžete vyřešit, stáhněte si dokončenou kapitolu a zkuste problém reprodukovat. Obecně můžete najít řešení problému porovnáním kódu s dokončeným kódem. Informace o některých běžných chybách a jejich řešení najdete v tématu Chyby a alternativní řešení.
V předchozích dvou kurzech jste pracovali se souvisejícími daty. V tomto kurzu se dozvíte, jak zpracovat souběžnost. Vytvoříte webové stránky, které pracují s entitou Department
, a stránky, které upravují a odstraňují Department
entity, budou zpracovávat chyby souběžnosti. Následující ilustrace znázorňují stránky Index a Odstranit, včetně některých zpráv, které se zobrazí, pokud dojde ke konfliktu souběžnosti.
Konflikty souběžnosti
Ke konfliktu souběžnosti dojde, když jeden uživatel zobrazí data entity, aby je mohl upravit, a pak jiný uživatel aktualizuje data stejné entity před zápisem změny prvního uživatele do databáze. Pokud nepovolíte detekci takových konfliktů, každý, kdo aktualizuje databázi, naposledy přepíše změny jiného uživatele. V mnoha aplikacích je toto riziko přijatelné: pokud existuje několik uživatelů nebo několik aktualizací, nebo pokud není ve skutečnosti důležité, pokud jsou některé změny přepsány, náklady na programování pro souběžnost můžou výhodu převažovat. V takovém případě nemusíte aplikaci konfigurovat tak, aby zpracovávala konflikty souběžnosti.
Pesimistické souběžnosti (uzamykání)
Pokud vaše aplikace potřebuje zabránit náhodné ztrátě dat ve scénářích souběžnosti, jedním ze způsobů, jak to udělat, je použít zámky databáze. Tomu se říká pesimistické souběžnost. Například před čtením řádku z databáze si vyžádáte zámek jen pro čtení nebo pro přístup k aktualizaci. Pokud uzamknete řádek pro přístup k aktualizacím, nebudou moct ostatní uživatelé zamknout řádek pro přístup jen pro čtení nebo pro aktualizaci, protože by získali kopii dat, která se právě mění. Pokud uzamknete řádek pro přístup jen pro čtení, ostatní ho můžou také uzamknout pro přístup jen pro čtení, ale ne pro aktualizaci.
Správa zámků má nevýhody. Program může být složitý. Vyžaduje významné prostředky pro správu databází a může způsobit problémy s výkonem při nárůstu počtu uživatelů aplikace (to znamená, že se dobře neššíruje). Z těchto důvodů ne všechny systémy pro správu databází podporují pesimistické souběžnosti. Entity Framework neposkytuje žádnou integrovanou podporu a tento kurz vám neukazuje, jak ji implementovat.
Optimistická metoda souběžného zpracování
Alternativou k pesimistické souběžnosti je optimistická souběžnost. Optimistická souběžnost znamená, že umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reagovat, pokud ano. Jan například spustí stránku Úpravy oddělení, změní částku rozpočtu pro anglické oddělení z 350 000,00 USD na 0,00 USD.
Než Jan klikne na Uložit, jane spustí stejnou stránku a změní pole Počáteční datum od 1. 9. 2007 do 8. 8. 2013.
Jan nejprve klikne na Uložit a uvidí změnu, když se prohlížeč vrátí na stránku Index a pak Jane klikne na Uložit. Co se stane dál, určuje způsob zpracování konfliktů souběžnosti. Mezi tyto možnosti patří:
Můžete sledovat, kterou vlastnost uživatel upravil, a aktualizovat pouze odpovídající sloupce v databázi. V ukázkovém scénáři by se neztratila žádná data, protože dva uživatelé aktualizovali různé vlastnosti. Když někdo příště přejde do anglického oddělení, uvidí změny Johna i Janeho – počáteční datum 8. 8. 2013 a rozpočet nula dolarů.
Tato metoda aktualizace může snížit počet konfliktů, které můžou vést ke ztrátě dat, ale nemůže se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti entity provedeny konkurenční změny. To, jestli Entity Framework funguje tímto způsobem, závisí na tom, jak implementujete aktualizační kód. Často to není praktické ve webové aplikaci, protože může vyžadovat, abyste zachovali velké množství stavu, abyste mohli sledovat všechny původní hodnoty vlastností pro entitu i nové hodnoty. Udržování velkého množství stavu může mít vliv na výkon aplikace, protože vyžaduje prostředky serveru nebo musí být součástí samotné webové stránky (například ve skrytých polích).
Janovu změnu můžete nechat přepsat. Když někdo příště přejde do anglického oddělení, uvidí 8.8.2013 a obnovenou hodnotu 350 000,00 USD. To se nazývá klient wins nebo last ve scénáři Wins . (Hodnoty klienta mají přednost před tím, co je v úložišti dat.) Jak je uvedeno v úvodu do této části, pokud neproděláte žádné kódování pro zpracování souběžnosti, dojde k tomu automaticky.
V databázi můžete zabránit aktualizaci janovy změny. Obvykle se zobrazí chybová zpráva, zobrazí se její aktuální stav dat a umožníte jí znovu použít její změny, pokud je stále chce provést. Tomu se říká scénář wins ve Storu. (Hodnoty úložiště dat mají přednost před hodnotami odeslanými klientem.) V tomto kurzu implementujete scénář Wins pro Store. Tato metoda zajišťuje, aby se nepřepsaly žádné změny, aniž by uživatel upozorňoval na to, co se děje.
Zjišťování konfliktů souběžnosti
Konflikty můžete vyřešit zpracováním výjimek OptimisticConcurrencyException , které entity Framework vyvolává. Aby bylo možné zjistit, kdy tyto výjimky vyvolat, musí být Entity Framework schopen detekovat konflikty. Proto je nutné správně nakonfigurovat databázi a datový model. Mezi možnosti povolení detekce konfliktů patří:
V tabulce databáze zahrňte sledovací sloupec, který lze použít k určení, kdy byl řádek změněn. Potom můžete rozhraní Entity Framework nakonfigurovat tak, aby zahrnovalo tento sloupec v
Where
klauzuli SQLUpdate
neboDelete
příkazů.Datový typ sloupce sledování je obvykle rowversion. Hodnota rowversion je pořadové číslo, které se při každé aktualizaci řádku zvýší.
Update
V klauzuli neboDelete
příkazWhere
obsahuje původní hodnotu sloupce sledování (původní verze řádku). Pokud byl řádek aktualizovaný jiným uživatelem změněn, hodnota verowversion
sloupci se liší od původní hodnoty, takže příkaz nemůžeDelete
najít řádek, kterýUpdate
se má aktualizovat kvůliWhere
klauzuli. Když Entity Framework zjistí, že žádné řádky nebyly aktualizovány příkazem neboDelete
příkazemUpdate
(to znamená, že počet ovlivněných řádků je nula), interpretuje to jako konflikt souběžnosti.Nakonfigurujte Entity Framework tak, aby zahrnovala původní hodnoty každého sloupce v tabulce v
Where
klauzuliUpdate
aDelete
příkazy.Stejně jako v první možnosti, pokud se od prvního čtení řádku něco v řádku změnilo,
Where
klauzule nevrátí řádek pro aktualizaci, který Entity Framework interpretuje jako konflikt souběžnosti. U databázových tabulek s mnoha sloupci může tento přístup vést k velmi velkýmWhere
klauzulemi a může vyžadovat, abyste zachovali velké množství stavu. Jak už jsme uvedli dříve, udržování velkého množství stavu může ovlivnit výkon aplikace, protože vyžaduje prostředky serveru nebo musí být součástí samotné webové stránky. Tento přístup se proto obecně nedoporučuje a není to metoda použitá v tomto kurzu.Pokud chcete tento přístup implementovat ke souběžnosti, je nutné označit všechny vlastnosti jiného než primárního klíče v entitě, pro kterou chcete sledovat souběžnost, přidáním atributu ConcurrencyCheck do nich. Tato změna umožňuje rozhraní Entity Framework zahrnout všechny sloupce do klauzule
UPDATE
SQLWHERE
příkazů.
Ve zbývající části tohoto kurzu přidáte do Department
entity vlastnost sledování rowversion, vytvoříte kontroler a zobrazení a otestujete, jestli všechno funguje správně.
Přidání vlastnosti optimistické souběžnosti do entity oddělení
Do pole Models\Department.cs přidejte vlastnost sledování s názvem RowVersion
:
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
public DateTime StartDate { get; set; }
[Display(Name = "Administrator")]
public int? InstructorID { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
Atribut časového razítka určuje, že tento sloupec bude zahrnut do Where
klauzule Update
a Delete
příkazů odesílaných do databáze. Atribut se nazývá Časové razítko, protože předchozí verze SQL Serveru používaly datový typ časového razítka SQL před nahrazením verze řádku SQL. Typ .Net pro rowversion
je bajtové pole. Pokud raději používáte rozhraní FLUENT API, můžete pomocí metody IsConcurrencyToken určit vlastnost sledování, jak je znázorněno v následujícím příkladu:
modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();
Podívejte se na problém GitHubu Replace IsConcurrencyToken by IsRowVersion.
Přidáním vlastnosti, kterou jste změnili model databáze, takže je potřeba provést další migraci. V konzole Správce balíčků (PMC) zadejte následující příkazy:
Add-Migration RowVersion
Update-Database
Vytvoření kontroleru oddělení
Pomocí následujících nastavení vytvořte Department
kontroler a zobrazení stejným způsobem jako ostatní kontrolery:
Do pole Controllers\DepartmentController.cs přidejte using
příkaz:
using System.Data.Entity.Infrastructure;
Změňte "LastName" na "FullName" všude v tomto souboru (čtyři výskyty), aby rozevírací seznamy správců oddělení obsahovaly celé jméno instruktora, a ne jenom příjmení.
ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName");
Nahraďte stávající kód pro metodu HttpPost
Edit
následujícím kódem:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")]
Department department)
{
try
{
if (ModelState.IsValid)
{
db.Entry(department).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().ToObject();
if (databaseValues.Name != clientValues.Name)
ModelState.AddModelError("Name", "Current value: "
+ databaseValues.Name);
if (databaseValues.Budget != clientValues.Budget)
ModelState.AddModelError("Budget", "Current value: "
+ String.Format("{0:c}", databaseValues.Budget));
if (databaseValues.StartDate != clientValues.StartDate)
ModelState.AddModelError("StartDate", "Current value: "
+ String.Format("{0:d}", databaseValues.StartDate));
if (databaseValues.InstructorID != clientValues.InstructorID)
ModelState.AddModelError("InstructorID", "Current value: "
+ db.Instructors.Find(databaseValues.InstructorID).FullName);
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
department.RowVersion = databaseValues.RowVersion;
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName", department.InstructorID);
return View(department);
}
Zobrazení uloží původní RowVersion
hodnotu do skrytého pole. Když binder modelu vytvoří department
instanci, tento objekt bude mít původní RowVersion
hodnotu vlastnosti a nové hodnoty pro ostatní vlastnosti, jak zadal uživatel na stránce Upravit. Když pak Entity Framework vytvoří příkaz SQL UPDATE
, bude tento příkaz obsahovat WHERE
klauzuli, která hledá řádek s původní RowVersion
hodnotou.
Pokud příkaz UPDATE
neovlivní žádné řádky (žádné řádky nemají původní RowVersion
hodnotu), Entity Framework vyvolá DbUpdateConcurrencyException
výjimku a kód v catch
bloku získá ovlivněnou Department
entitu z objektu výjimky. Tato entita má hodnoty načtené z databáze i nové hodnoty zadané uživatelem:
var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().ToObject();
Dále kód přidá vlastní chybovou zprávu pro každý sloupec s hodnotami databáze, které se liší od toho, co uživatel zadal na stránce Upravit:
if (databaseValues.Name != currentValues.Name)
ModelState.AddModelError("Name", "Current value: " + databaseValues.Name);
// ...
Delší chybová zpráva vysvětluje, co se stalo a co s tím dělat:
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The"
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
Nakonec kód nastaví RowVersion
hodnotu objektu Department
na novou hodnotu načtenou z databáze. Tato nová RowVersion
hodnota se uloží do skrytého pole při opětovném zobrazení stránky Pro úpravy a při příštím kliknutí uživatele na Uložit dojde pouze k chybám souběžnosti, ke kterým dochází, protože se při přehrání stránky Pro úpravy zachytí.
V Views\Department\Edit.cshtml přidejte skryté pole pro uložení RowVersion
hodnoty vlastnosti bezprostředně za skryté pole vlastnosti DepartmentID
:
@model ContosoUniversity.Models.Department
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<fieldset>
<legend>Department</legend>
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
V zobrazení\Department\Index.cshtml nahraďte stávající kód následujícím kódem, kterým přesunete odkazy na řádky doleva a změníte nadpis stránky a záhlaví sloupců tak, aby se zobrazovaly FullName
místo LastName
ve sloupci Správce :
@model IEnumerable<ContosoUniversity.Models.Department>
@{
ViewBag.Title = "Departments";
}
<h2>Departments</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th></th>
<th>Name</th>
<th>Budget</th>
<th>Start Date</th>
<th>Administrator</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.DepartmentID }) |
@Html.ActionLink("Details", "Details", new { id=item.DepartmentID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.DepartmentID })
</td>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Administrator.FullName)
</td>
</tr>
}
</table>
Testování optimistického zpracování souběžnosti
Spusťte web a klikněte na Oddělení:
Klikněte pravým tlačítkem myši na hypertextový odkaz Pro Kim Abercrombie a vyberte Otevřít v nové kartě a potom klikněte na upravit hypertextový odkaz pro Kim Abercrombie. Dvě okna zobrazují stejné informace.
Změňte pole v prvním okně prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou.
Změňte libovolné pole v druhém okně prohlížeče a klikněte na Uložit.
Klikněte na Uložit v druhém okně prohlížeče. Zobrazí se chybová zpráva:
Znovu klikněte na Uložit . Hodnota zadaná v druhém prohlížeči se uloží spolu s původní hodnotou dat, která změníte v prvním prohlížeči. Uložené hodnoty se zobrazí, když se zobrazí stránka Index.
Aktualizace stránky Pro odstranění
U stránky Odstranit služba Entity Framework detekuje konflikty souběžnosti způsobené jiným uživatelem, který upravuje oddělení podobným způsobem. Když metoda HttpGet
Delete
zobrazí potvrzovací zobrazení, zobrazení obsahuje původní RowVersion
hodnotu ve skrytém poli. Tato hodnota je pak k dispozici metodě HttpPost
Delete
, která se volá, když uživatel potvrdí odstranění. Když Entity Framework vytvoří příkaz SQL DELETE
, obsahuje WHERE
klauzuli s původní RowVersion
hodnotou. Pokud má příkaz za následek nulový dopad na řádky (to znamená, že se řádek po zobrazení stránky potvrzení odstranění změnil), vyvolá se výjimka souběžnosti a HttpGet Delete
volá se metoda s příznakem chyby nastaveným na true
opětovné zobrazení potvrzovací stránky s chybovou zprávou. Je také možné, že byly ovlivněny nulové řádky, protože řádek odstranil jiný uživatel, takže v takovém případě se zobrazí jiná chybová zpráva.
V DepartmentController.cs nahraďte metodu HttpGet
Delete
následujícím kódem:
public ActionResult Delete(int id, bool? concurrencyError)
{
Department department = db.Departments.Find(id);
if (concurrencyError.GetValueOrDefault())
{
if (department == null)
{
ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was deleted by another user after you got the original values. "
+ "Click the Back to List hyperlink.";
}
else
{
ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you got the original values. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again. Otherwise "
+ "click the Back to List hyperlink.";
}
}
return View(department);
}
Metoda přijímá volitelný parametr, který označuje, zda se stránka znovu zobrazuje po chybě souběžnosti. Pokud je true
tento příznak , chybová zpráva se odešle do zobrazení pomocí ViewBag
vlastnosti.
Nahraďte kód v HttpPost
Delete
metodě (pojmenovaný DeleteConfirmed
) následujícím kódem:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(Department department)
{
try
{
db.Entry(department).State = EntityState.Deleted;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToAction("Delete", new { concurrencyError=true } );
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
return View(department);
}
}
V vygenerovaný kód, který jste právě nahradili, tato metoda přijala pouze ID záznamu:
public ActionResult DeleteConfirmed(int id)
Tento parametr jste změnili na Department
instanci entity vytvořenou pořadačem modelu. Tím získáte přístup k hodnotě RowVersion
vlastnosti kromě klíče záznamu.
public ActionResult Delete(Department department)
Změnili jste také název metody akce z DeleteConfirmed
na Delete
. Vygenerovaný kód pojmenovaný metodou HttpPost
Delete
DeleteConfirmed
HttpPost
, který metodě poskytne jedinečný podpis. (CLR vyžaduje přetížené metody, aby měly různé parametry metody.) Teď, když jsou podpisy jedinečné, můžete držet konvence MVC a použít stejný název pro HttpPost
metody a HttpGet
metody delete.
Pokud dojde k chybě souběžnosti, kód znovu zobrazí potvrzovací stránku odstranění a zobrazí příznak, který označuje, že by se měla zobrazit chybová zpráva souběžnosti.
V views\Department\Delete.cshtml nahraďte vygenerovaný kód následujícím kódem, který provede určité změny formátování a přidá pole chybové zprávy. Změny jsou zvýrazněné.
@model ContosoUniversity.Models.Department
@{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<p class="error">@ViewBag.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<fieldset>
<legend>Department</legend>
<div class="display-label">
@Html.DisplayNameFor(model => model.Name)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
<div class="display-label">
@Html.DisplayNameFor(model => model.Budget)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Budget)
</div>
<div class="display-label">
@Html.DisplayNameFor(model => model.StartDate)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.StartDate)
</div>
<div class="display-label">
@Html.DisplayNameFor(model => model.Administrator.FullName)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Administrator.FullName)
</div>
</fieldset>
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
<p>
<input type="submit" value="Delete" /> |
@Html.ActionLink("Back to List", "Index")
</p>
}
Tento kód přidá mezi nadpisy chybovou h2
h3
zprávu:
<p class="error">@ViewBag.ConcurrencyErrorMessage</p>
LastName
Nahradí ho FullName
Administrator
v poli:
<div class="display-label">
@Html.LabelFor(model => model.InstructorID)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Administrator.FullName)
</div>
Nakonec za příkaz přidá skrytá pole a DepartmentID
RowVersion
vlastnosti Html.BeginForm
:
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
Spusťte stránku Index oddělení. Klikněte pravým tlačítkem myši na odstranit hypertextový odkaz pro anglické oddělení a vyberte Otevřít v novém okně a pak v prvním okně klikněte na upravit hypertextový odkaz pro anglické oddělení.
V prvním okně změňte jednu z hodnot a klikněte na Uložit :
Stránka Index potvrdí změnu.
V druhém okně klikněte na Odstranit.
Zobrazí se chybová zpráva o souběžnosti a hodnoty oddělení se aktualizují s informacemi, které jsou aktuálně v databázi.
Pokud znovu kliknete na Odstranit , budete přesměrováni na stránku Index, která ukazuje, že oddělení bylo odstraněno.
Shrnutí
Tím se dokončí úvod ke zpracování konfliktů souběžnosti. Informace o dalších způsobech zpracování různých scénářů souběžnosti najdete v tématu Optimistic concurrency Patterns and Working with Property Values na blogu týmu Entity Framework. V dalším kurzu se dozvíte, jak implementovat dědičnost tabulek podle hierarchie pro entity Instructor
a Student
entity.
Odkazy na další prostředky Entity Framework najdete v mapě obsahu ASP.NET Data Accessu.