Část 8, Razor Stránky s EF Core ASP.NET Core – souběžnost
Webová aplikace Contoso University ukazuje, jak vytvářet Razor webové aplikace Pages pomocí EF Core sady Visual Studio. Informace o sérii kurzů najdete v prvním kurzu.
Pokud narazíte na problémy, které nemůžete vyřešit, stáhněte si dokončenou aplikaci a porovnejte tento kód s tím, co jste vytvořili podle kurzu.
V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje entitu souběžně.
Konflikty souběžnosti
Ke konfliktu souběžnosti dochází v případě, že:
- Uživatel přejde na stránku pro úpravy entity.
- Jiný uživatel aktualizuje stejnou entitu před zápisem změny prvního uživatele do databáze.
Pokud detekce souběžnosti není povolená, přepíše změny ostatních uživatelů každý, kdo aktualizuje databázi. Pokud je toto riziko přijatelné, náklady na programování pro souběžnost můžou převažovat nad výhodou.
Pesimistické souběžnosti
Jedním zezpůsobůch Tomu se říká pesimistické souběžnost. Než aplikace přečte řádek databáze, který má v úmyslu aktualizovat, požádá o zámek. Jakmile je řádek uzamčen pro přístup k aktualizacím, nebudou moct ostatní uživatelé řádek uzamknout, dokud nebude vydán první zámek.
Správa zámků má nevýhody. Program může být složitý a může způsobovat problémy s výkonem při nárůstu počtu uživatelů. Entity Framework Core neposkytuje žádnou integrovanou podporu pesimistické souběžnosti.
Optimistická metoda souběžného zpracování
Optimistická souběžnost umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reaguje, když ano. Jane například navštíví stránku pro úpravy oddělení a změní rozpočet pro anglické oddělení z 350 000,00 USD na 0,00 USD.
Než Jane klikne na Uložit, jan navštíví stejnou stránku a změní pole Počáteční datum od 1. 9. 2007 do 1. 9. 2013.
Jane nejprve klikne na Uložit a uvidí, že se její změna projeví, protože v prohlížeči se jako částka rozpočtu zobrazí stránka Index s nulou.
Jan klikne na uložit na stránce Pro úpravy, která stále zobrazuje rozpočet 350 000,00 USD. Co se stane dál, určuje způsob zpracování konfliktů souběžnosti:
Sledujte, kterou vlastnost uživatel upravil, a aktualizujte pouze odpovídající sloupce v databázi.
V tomto scénáři nedojde ke ztrátě žádných dat. Dva uživatelé aktualizovali různé vlastnosti. Když někdo příště přejde do anglického oddělení, uvidí změny Jane i Johna. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat. Tento přístup má určité nevýhody:
- Nelze se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti provedeny konkurenční změny.
- Obecně není praktické ve webové aplikaci. Vyžaduje udržování významného stavu, aby bylo možné sledovat všechny načtené hodnoty a nové hodnoty. Udržování velkého objemu stavu může ovlivnit výkon aplikace.
- Může zvýšit složitost aplikace v porovnání s detekcí souběžnosti u entity.
Přepíše janovu změnu.
Když někdo příště přejde do anglického oddělení, uvidí 1. 9. 2013 a načte hodnotu 350 000,00 USD. Tento přístup se označuje jako klient wins nebo last ve scénáři Wins . Všechny hodnoty z klienta mají přednost před tím, co je v úložišti dat. Vygenerovaný kód nezpracovává žádné zpracování souběžnosti, klient wins se provádí automaticky.
Znemožnit aktualizaci Janovy změny v databázi. Aplikace obvykle:
- Zobrazí chybovou zprávu.
- Zobrazí aktuální stav dat.
- Umožňuje uživateli znovu použít změny.
Tomu se říká scénář wins ve Storu. Hodnoty úložiště dat mají přednost před hodnotami odeslanými klientem. Scénář Wins pro Store se používá v tomto kurzu. Tato metoda zajišťuje, že se bez upozornění uživatele nepřepíší žádné změny.
Detekce konfliktů v EF Core
Vlastnosti nakonfigurované jako tokeny souběžnosti se používají k implementaci optimistického řízení souběžnosti. Pokud je operace aktualizace nebo odstranění aktivována SaveChanges nebo SaveChangesAsync, hodnota tokenu souběžnosti v databázi se porovná s původní hodnotou přečtenou EF Core:
- Pokud se hodnoty shodují, operace se může dokončit.
- Pokud se hodnoty neshodují, předpokládá, EF Core že jiný uživatel provedl konfliktní operaci, přeruší aktuální transakci a vyvolá DbUpdateConcurrencyException.
Jiný uživatel nebo proces provádějící operaci, která je v konfliktu s aktuální operací, se označuje jako konflikt souběžnosti.
V relačních databázích EF Core kontroluje hodnotu tokenu souběžnosti v WHERE
klauzuli UPDATE
a DELETE
příkazech za účelem zjištění konfliktu souběžnosti.
Datový model musí být nakonfigurovaný tak, aby umožňoval detekci konfliktů zahrnutím sloupce sledování, který lze použít k určení, kdy byl řádek změněn. EF poskytuje dva přístupy pro tokeny souběžnosti:
[ConcurrencyCheck]
Použití nebo IsConcurrencyToken na vlastnost v modelu Tento přístup se nedoporučuje. Další informace naleznete v tématu Tokeny souběžnosti v EF Core.TimestampAttribute Použití tokenu souběžnosti v modelu nebo IsRowVersion na token souběžnosti Toto je přístup použitý v tomto kurzu.
Podrobnosti o implementaci SQL Serveru a SQLite se mírně liší. Soubor rozdílů se zobrazí později v kurzu se seznamem rozdílů. Na kartě Visual Studio se zobrazuje přístup k SQL Serveru. Na kartě Visual Studio Code se zobrazuje přístup k databázím, které nejsou sql Serverem, jako je například SQLite.
- Do modelu zahrňte sledovací sloupec, který slouží k určení, kdy byl řádek změněn.
- TimestampAttribute Použijte vlastnost souběžnosti.
Models/Department.cs
Aktualizujte soubor následujícím zvýrazněným kódem:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
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)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] ConcurrencyToken { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Jedná se o TimestampAttribute to, co identifikuje sloupec jako sloupec sledování souběžnosti. Fluent API je alternativní způsob, jak určit vlastnost sledování:
modelBuilder.Entity<Department>()
.Property<byte[]>("ConcurrencyToken")
.IsRowVersion();
Atribut [Timestamp]
vlastnosti entity vygeneruje následující kód v ModelBuilder metodě:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Předchozí kód:
- Nastaví typ
ConcurrencyToken
vlastnosti na pole bajtů.byte[]
je požadovaný typ pro SQL Server. - Zavolá metodu IsConcurrencyToken.
IsConcurrencyToken
nakonfiguruje vlastnost jako token souběžnosti. Při aktualizacích se hodnota tokenu souběžnosti v databázi porovnává s původní hodnotou, aby se od načtení instance z databáze nezměnila. Pokud došlo ke změně, DbUpdateConcurrencyException vyvolá se a změny se nepoužijí. - Volání ValueGeneratedOnAddOrUpdate, která konfiguruje vlastnost tak, aby měla hodnotu automaticky vygenerovanou
ConcurrencyToken
při přidání nebo aktualizaci entity. HasColumnType("rowversion")
nastaví typ sloupce v databázi SQL Serveru na rowversion.
Následující kód ukazuje část T-SQL vygenerovanou při EF Core Department
aktualizaci názvu:
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Předchozí zvýrazněný kód zobrazuje klauzuli WHERE
obsahující ConcurrencyToken
. Pokud se databáze ConcurrencyToken
nerovná parametru@p2
, nebudou ConcurrencyToken
aktualizovány žádné řádky.
Následující zvýrazněný kód ukazuje T-SQL, který ověřuje, že byl aktualizován přesně jeden řádek:
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT vrátí počet řádků ovlivněných posledním příkazem. Pokud nejsou aktualizovány žádné řádkyDbUpdateConcurrencyException
, EF Core vyvolá chybu .
Přidání migrace
ConcurrencyToken
Přidání vlastnosti změní datový model, který vyžaduje migraci.
Sestavte projekt.
V PMC spusťte následující příkazy:
Add-Migration RowVersion
Update-Database
Předchozí příkazy:
Migrations/{time stamp}_RowVersion.cs
Vytvoří soubor migrace.Migrations/SchoolContextModelSnapshot.cs
Aktualizuje soubor. Aktualizace přidá doBuildModel
metody následující kód:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Stránky oddělení uživatelského rozhraní
Postupujte podle pokynů na stránkách studentů uživatelského rozhraní s následujícími výjimkami:
- Vytvořte složku Pages/Departments .
- Používá
Department
se pro třídu modelu. - Místo vytvoření nové třídy použijte existující třídu kontextu.
Přidání třídy utility
Ve složce projektu vytvořte Utility
třídu s následujícím kódem:
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
Třída Utility
poskytuje metodu GetLastChars
použitou k zobrazení posledních několika znaků tokenu souběžnosti. Následující kód ukazuje kód, který funguje s oběma službami SQLite ad SQL Server:
#if SQLiteVersion
using System;
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(Guid token)
{
return token.ToString().Substring(
token.ToString().Length - 3);
}
}
}
#else
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
#endif
Direktiva #if SQLiteVersion
preprocesoru izoluje rozdíly ve verzích SQLite a SQL Server a pomáhá:
- Autor udržuje pro obě verze jeden základ kódu.
- Vývojáři SQLite nasazují aplikaci do Azure a používají SQL Azure.
Sestavte projekt.
Aktualizace indexové stránky
Nástroj pro generování uživatelského rozhraní vytvořil ConcurrencyToken
sloupec pro indexovou stránku, ale toto pole se v produkční aplikaci nezobrazí. V tomto kurzu se zobrazí poslední část tohoto ConcurrencyToken
kurzu, která vám pomůže ukázat, jak funguje zpracování souběžnosti. Poslední část není zaručená, že bude jedinečná sama o sobě.
Aktualizovat stránky\Departments\Index.cshtml :
- Nahraďte index odděleními.
- Změňte kód obsahující
ConcurrencyToken
jenom několik posledních znaků. - Nahraďte
FirstMidName
FullName
.
Aktualizovaná stránka se zobrazí v následujícím kódu:
@page
@model ContosoUniversity.Pages.Departments.IndexModel
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Administrator)
</th>
<th>
Token
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department)
{
<tr>
<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>
<td>
@Utility.GetLastChars(item.ConcurrencyToken)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Aktualizace modelu upravit stránku
Aktualizujte Pages/Departments/Edit.cshtml.cs
následujícím kódem:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch current department from DB.
// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Set ConcurrencyToken to value read in OnGetAsync
_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current ConcurrencyToken so next postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private IActionResult HandleDeletedDepartment()
{
// ModelState contains the posted data because of the deletion error
// and overides the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task SetDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
}
}
Aktualizace souběžnosti
OriginalValue se aktualizuje o ConcurrencyToken
hodnotu z entity, když byla načtena v OnGetAsync
metodě. EF Core vygeneruje SQL UPDATE
příkaz s WHERE
klauzulí obsahující původní ConcurrencyToken
hodnotu. Pokud příkaz UPDATE
neobsahuje žádné řádky, DbUpdateConcurrencyException
vyvolá se výjimka. Příkaz nemá vliv na UPDATE
žádné řádky, pokud žádné řádky nemají původní ConcurrencyToken
hodnotu.
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch current department from DB.
// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Set ConcurrencyToken to value read in OnGetAsync
_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
V předchozím zvýrazněném kódu:
- Hodnota je
Department.ConcurrencyToken
hodnota, když byla entita načtena vGet
požadavku naEdit
stránku. Tato hodnota je pro metoduOnPost
poskytována skrytým polem na Razor stránce, která zobrazuje entitu, která se má upravit. Skrytá hodnota pole se zkopíruje doDepartment.ConcurrencyToken
pořadače modelu. OriginalValue
je to, co EF Core se používá v klauzuliWHERE
. Před spuštěním zvýrazněného řádku kódu:OriginalValue
má hodnotu, která byla v databázi, kdyžFirstOrDefaultAsync
byla volána v této metodě.- Tato hodnota se může lišit od toho, co se zobrazilo na stránce Upravit.
- Zvýrazněný kód zajišťuje, že EF Core používá původní
ConcurrencyToken
hodnotu ze zobrazenéDepartment
entity v klauzuli příkazuWHERE
SQLUPDATE
.
Následující kód ukazuje Department
model. Department
inicializuje se v:
OnGetAsync
metodou podle dotazu EF.OnPostAsync
metoda podle skrytého pole na Razor stránce pomocí vazby modelu:
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch current department from DB.
// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Set ConcurrencyToken to value read in OnGetAsync
_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
Předchozí kód ukazuje ConcurrencyToken
hodnotu entity z HTTP POST
požadavku je nastavena na ConcurrencyToken
hodnotu z HTTP GET
požadavku.Department
Když dojde k chybě souběžnosti, získá následující zvýrazněný kód hodnoty klienta (hodnoty publikované do této metody) a hodnoty databáze.
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current ConcurrencyToken so next postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
}
Následující kód přidá vlastní chybovou zprávu pro každý sloupec, který má hodnoty databáze odlišné od toho, co bylo publikováno:OnPostAsync
private async Task SetDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
Následující zvýrazněný kód nastaví ConcurrencyToken
hodnotu na novou hodnotu načtenou z databáze. Když uživatel příště klikne na Uložit, budou zachyceny pouze chyby souběžnosti, ke kterým dochází od posledního zobrazení stránky Upravit.
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current ConcurrencyToken so next postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
}
Příkaz ModelState.Remove
je povinný, protože ModelState
má předchozí ConcurrencyToken
hodnotu. Razor Na stránce ModelState
má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici obě hodnoty.
Rozdíly v kódu SQL Serveru vs. SQLite
Následující informace ukazují rozdíly mezi verzemi SQL Serveru a SQLite:
+ using System; // For GUID on SQLite
+ departmentToUpdate.ConcurrencyToken = Guid.NewGuid();
_context.Entry(departmentToUpdate)
.Property(d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
- Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
+ Department.ConcurrencyToken = dbValues.ConcurrencyToken;
Aktualizace stránky Upravit Razor
Aktualizujte Pages/Departments/Edit.cshtml
následujícím kódem:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.ConcurrencyToken" />
<div class="form-group">
<label>Version</label>
@Utility.GetLastChars(Model.Department.ConcurrencyToken)
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label"></label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label"></label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label"></label>
<input asp-for="Department.StartDate" class="form-control" />
<span asp-validation-for="Department.StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Předchozí kód:
- Aktualizuje direktivu
page
od@page
@page "{id:int}"
- Přidá skrytou verzi řádku.
ConcurrencyToken
musí být přidán, takže postback sváže hodnotu. - Zobrazí poslední bajt
ConcurrencyToken
pro účely ladění. ViewData
Nahrazuje silným typemInstructorNameSL
.
Konflikty souběžnosti testů se stránkou Upravit
Otevřete dvě instance prohlížeče pro úpravy v anglickém oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Pro anglické oddělení a vyberte Otevřít v nové kartě.
- Na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte název na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným ConcurrencyToken
indikátorem. Všimněte si aktualizovaného ConcurrencyToken
indikátoru, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Změňte jiné pole na druhé kartě prohlížeče.
Klikněte na Uložit. Zobrazí se chybové zprávy pro všechna pole, která neodpovídají hodnotám databáze:
Toto okno prohlížeče nemělo v úmyslu změnit pole Název. Zkopírujte a vložte aktuální hodnotu (Jazyky) do pole Název. Vysouvte tabulátor. Ověření na straně klienta odebere chybovou zprávu.
Znovu klikněte na Uložit . Hodnota, kterou jste zadali na druhé kartě prohlížeče, se uloží. Uložené hodnoty se zobrazí na stránce Index.
Aktualizace modelu odstranění stránky
Aktualizujte Pages/Departments/Delete.cshtml.cs
následujícím kódem:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "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.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.ConcurrencyToken value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}
Stránka Delete detekuje konflikty souběžnosti, když se entita po načtení změnila. Department.ConcurrencyToken
je verze řádku, když byla entita načtena. Při EF Core vytváření SQL DELETE
příkazu obsahuje klauzuli WHERE s ConcurrencyToken
. Pokud má SQL DELETE
příkaz za následek nulový počet ovlivněných řádků:
- Příkaz
ConcurrencyToken
seSQL DELETE
v databázi neshodujeConcurrencyToken
. - Vyvolá
DbUpdateConcurrencyException
se výjimka. OnGetAsync
je volána sconcurrencyError
.
Aktualizace stránky Odstranit Razor
Aktualizujte Pages/Departments/Delete.cshtml
následujícím kódem:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.ConcurrencyToken)
</dt>
<dd class="col-sm-10">
@Utility.GetLastChars(Model.Department.ConcurrencyToken)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Administrator.FullName)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.ConcurrencyToken" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
Předchozí kód provede následující změny:
- Aktualizuje direktivu
page
od@page
@page "{id:int}"
- Přidá chybovou zprávu.
- Nahradí Jméno FirstMidName úplným názvem v poli Správce .
- Změny
ConcurrencyToken
pro zobrazení posledního bajtu - Přidá skrytou verzi řádku.
ConcurrencyToken
musí být přidán, takže postback sváže hodnotu.
Konflikty souběžnosti testů
Vytvoření testovacího oddělení
Otevřete dvě instance odstranění v testovacím oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro testovací oddělení a vyberte Otevřít na nové kartě.
- Klikněte na odkaz Upravit hypertextový odkaz pro testovací oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte rozpočet na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným ConcurrencyToken
indikátorem. Všimněte si aktualizovaného ConcurrencyToken
indikátoru, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Odstraňte testovací oddělení z druhé karty. Zobrazí se chyba souběžnosti s aktuálními hodnotami z databáze. Kliknutím na Odstranit odstraníte entitu, pokud ConcurrencyToken
nebyla aktualizována.
Další materiály
Další kroky
Toto je poslední kurz série. Další témata jsou popsána ve verzi MVC této série kurzů.
V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje entitu souběžně (ve stejnou dobu).
Konflikty souběžnosti
Ke konfliktu souběžnosti dochází v případě, že:
- Uživatel přejde na stránku pro úpravy entity.
- Jiný uživatel aktualizuje stejnou entitu před zápisem změny prvního uživatele do databáze.
Pokud detekce souběžnosti není povolená, přepíše změny ostatních uživatelů každý, kdo aktualizuje databázi. Pokud je toto riziko přijatelné, náklady na programování pro souběžnost můžou převažovat nad výhodou.
Pesimistické souběžnost (uzamykání)
Jedním zezpůsobůch Tomu se říká pesimistické souběžnost. Než aplikace přečte řádek databáze, který má v úmyslu aktualizovat, požádá o zámek. Jakmile je řádek uzamčen pro přístup k aktualizacím, nebudou moct ostatní uživatelé řádek uzamknout, dokud nebude vydán první zámek.
Správa zámků má nevýhody. Program může být složitý a může způsobovat problémy s výkonem při nárůstu počtu uživatelů. Entity Framework Core neposkytuje žádnou integrovanou podporu a tento kurz neukazuje, jak ho implementovat.
Optimistická metoda souběžného zpracování
Optimistická souběžnost umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reaguje, když ano. Jane například navštíví stránku pro úpravy oddělení a změní rozpočet pro anglické oddělení z 350 000,00 USD na 0,00 USD.
Než Jane klikne na Uložit, jan navštíví stejnou stránku a změní pole Počáteční datum od 1. 9. 2007 do 1. 9. 2013.
Jane nejprve klikne na Uložit a uvidí, že se její změna projeví, protože v prohlížeči se jako částka rozpočtu zobrazí stránka Index s nulou.
Jan klikne na uložit na stránce Pro úpravy, která stále zobrazuje rozpočet 350 000,00 USD. Co se stane dál, určuje způsob zpracování konfliktů souběžnosti:
Můžete sledovat, kterou vlastnost uživatel upravil, a aktualizovat pouze odpovídající sloupce v databázi.
V tomto scénáři nedojde ke ztrátě žádných dat. Dva uživatelé aktualizovali různé vlastnosti. Když někdo příště přejde do anglického oddělení, uvidí změny Jane i Johna. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat. Tento přístup má určité nevýhody:
- Nelze se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti provedeny konkurenční změny.
- Obecně není praktické ve webové aplikaci. Vyžaduje udržování významného stavu, aby bylo možné sledovat všechny načtené hodnoty a nové hodnoty. Udržování velkého objemu stavu může ovlivnit výkon aplikace.
- Může zvýšit složitost aplikace v porovnání s detekcí souběžnosti u entity.
Johnovi můžete dát možnost přepsat Janovu změnu.
Když někdo příště přejde do anglického oddělení, uvidí 1. 9. 2013 a načte hodnotu 350 000,00 USD. Tento přístup se označuje jako klient wins nebo last ve scénáři Wins . (Všechny hodnoty z klienta mají přednost před tím, co je v úložišti dat.) Pokud neprovádíte žádné kódování pro zpracování souběžnosti, dojde k automatickému zpracování služby Client Wins.
V databázi můžete zabránit aktualizaci změny Johna. Aplikace obvykle:
- Zobrazí chybovou zprávu.
- Zobrazí aktuální stav dat.
- Umožňuje uživateli znovu použít změny.
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, že se bez upozornění uživatele nepřepíší žádné změny.
Detekce konfliktů v EF Core
EF CoreDbConcurrencyException
vyvolá výjimky, když zjistí konflikty. Datový model musí být nakonfigurovaný tak, aby umožňoval detekci konfliktů. Mezi možnosti povolení detekce konfliktů patří:
Nakonfigurujte EF Core tak, aby zahrnovaly původní hodnoty sloupců nakonfigurovaných jako tokeny souběžnosti v klauzuli Where příkazů Update a Delete.
Při
SaveChanges
zavolání klauzule Where vyhledá původní hodnoty všech vlastností anotovaných atributem ConcurrencyCheckAttribute . Příkaz update nenajde řádek, který se má aktualizovat, pokud se od prvního čtení řádku změnily některé vlastnosti tokenu souběžnosti. EF Core interpretuje to jako konflikt souběžnosti. U databázových tabulek s mnoha sloupci může tento přístup vést k velmi velkým klauzulem Where a může vyžadovat velké množství stavu. Proto se tento přístup obecně nedoporučuje a není to metoda použitá v tomto kurzu.V tabulce databáze zahrňte sledovací sloupec, který lze použít k určení, kdy byl řádek změněn.
V databázi SQL Serveru je datový typ sloupce
rowversion
sledování . Hodnotarowversion
je pořadové číslo, které se při každé aktualizaci řádku zvýší. V příkazu Update nebo Delete klauzule Where obsahuje původní hodnotu sloupce sledování (číslo původní verze řádku). Pokud byl řádek aktualizovaný jiným uživatelem změněn, hodnota verowversion
sloupci se liší od původní hodnoty. V takovém případě příkaz Update nebo Delete nemůže najít řádek, který se má aktualizovat kvůli klauzuli Where. EF Core vyvolá výjimku souběžnosti, pokud příkaz Update nebo Delete neovlivní žádné řádky.
Přidání vlastnosti sledování
V Models/Department.cs
aplikaci přidejte sledovací vlastnost s názvem RowVersion:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
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)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Atribut TimestampAttribute je to, co identifikuje sloupec jako sloupec sledování souběžnosti. Fluent API je alternativní způsob, jak určit vlastnost sledování:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
U databáze [Timestamp]
SQL Serveru atribut vlastnosti entity definované jako pole bajtů:
- Způsobí zahrnutí sloupce do klauzulí DELETE a UPDATE WHERE.
- Nastaví typ sloupce v databázi na rowversion.
Databáze vygeneruje sekvenční číslo verze řádku, které se zvýší při každé aktualizaci řádku. Update
V klauzuli nebo Delete
příkaz Where
obsahuje načtenou hodnotu verze řádku. Pokud se od načtení řádku změnil řádek:
- Hodnota aktuální verze řádku neodpovídá načtené hodnotě.
Delete
PříkazyUpdate
nenaleznou řádek, protožeWhere
klauzule hledá hodnotu verze načteného řádku.- A
DbUpdateConcurrencyException
je vyhozen.
Následující kód ukazuje část T-SQL vygenerovanou při EF Core aktualizaci názvu oddělení:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Předchozí zvýrazněný kód zobrazuje klauzuli WHERE
obsahující RowVersion
. Pokud se databáze RowVersion
nerovná parametru (@p2
), nebudou RowVersion
aktualizovány žádné řádky.
Následující zvýrazněný kód ukazuje T-SQL, který ověřuje, že byl aktualizován přesně jeden řádek:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT vrátí počet řádků ovlivněných posledním příkazem. Pokud nejsou aktualizovány žádné řádkyDbUpdateConcurrencyException
, EF Core vyvolá chybu .
Aktualizace databáze
RowVersion
Přidání vlastnosti změní datový model, který vyžaduje migraci.
Sestavte projekt.
V PMC spusťte následující příkaz:
Add-Migration RowVersion
Tento příkaz:
Migrations/{time stamp}_RowVersion.cs
Vytvoří soubor migrace.Migrations/SchoolContextModelSnapshot.cs
Aktualizuje soubor. Aktualizace přidá doBuildModel
metody následující zvýrazněný kód:modelBuilder.Entity("ContosoUniversity.Models.Department", b => { b.Property<int>("DepartmentID") .ValueGeneratedOnAdd() .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property<decimal>("Budget") .HasColumnType("money"); b.Property<int?>("InstructorID"); b.Property<string>("Name") .HasMaxLength(50); b.Property<byte[]>("RowVersion") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate(); b.Property<DateTime>("StartDate"); b.HasKey("DepartmentID"); b.HasIndex("InstructorID"); b.ToTable("Department"); });
V PMC spusťte následující příkaz:
Update-Database
Stránky oddělení uživatelského rozhraní
Postupujte podle pokynů na stránkách studentů uživatelského rozhraní s následujícími výjimkami:
Vytvořte složku Pages/Departments .
Používá
Department
se pro třídu modelu.- Místo vytvoření nové třídy použijte existující třídu kontextu.
Sestavte projekt.
Aktualizace indexové stránky
Nástroj pro generování uživatelského rozhraní vytvořil RowVersion
sloupec pro indexovou stránku, ale toto pole se v produkční aplikaci nezobrazí. V tomto kurzu se zobrazí poslední bajt RowVersion
bajtů, který vám pomůže ukázat, jak funguje zpracování souběžnosti. Poslední bajt není zaručený, že bude jedinečný sám.
Aktualizovat stránky\Departments\Index.cshtml :
- Nahraďte index odděleními.
- Změňte kód obsahující
RowVersion
tak, aby zobrazoval pouze poslední bajt pole bajtů. - Nahraďte FirstMidName úplným názvem.
Aktualizovaná stránka se zobrazí v následujícím kódu:
@page
@model ContosoUniversity.Pages.Departments.IndexModel
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Administrator)
</th>
<th>
RowVersion
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department)
{
<tr>
<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>
<td>
@item.RowVersion[7]
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Aktualizace modelu upravit stránku
Aktualizujte Pages/Departments/Edit.cshtml.cs
následujícím kódem:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private IActionResult HandleDeletedDepartment()
{
var deletedDepartment = new Department();
// ModelState contains the posted data because of the deletion error
// and will overide the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
}
}
Aktualizuje se OriginalValue o rowVersion
hodnotu z entity, když byla načtena v OnGetAsync
metodě. EF Core vygeneruje příkaz SQL UPDATE s klauzulí WHERE obsahující původní RowVersion
hodnotu. Pokud příkaz UPDATE neovlivní žádné řádky (žádné řádky nemají původní RowVersion
hodnotu), DbUpdateConcurrencyException
vyvolá se výjimka.
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
V předchozím zvýrazněném kódu:
- Hodnota je
Department.RowVersion
to, co byla v entitě, když byla původně načtena v požadavku Získat pro stránku Upravit. Tato hodnota je pro metoduOnPost
poskytována skrytým polem na Razor stránce, která zobrazuje entitu, která se má upravit. Skrytá hodnota pole se zkopíruje doDepartment.RowVersion
pořadače modelu. OriginalValue
je to, co EF Core se použije v klauzuli Where. Před spuštěním zvýrazněného řádku kódu má hodnotu,OriginalValue
která byla v databázi, kdyžFirstOrDefaultAsync
byla volána v této metodě, což se může lišit od toho, co se zobrazilo na stránce Upravit.- Zvýrazněný kód zajišťuje, že EF Core používá původní
RowVersion
hodnotu ze zobrazenéDepartment
entity v klauzuli Where příkazu SQL UPDATE.
Když dojde k chybě souběžnosti, získá následující zvýrazněný kód hodnoty klienta (hodnoty publikované do této metody) a hodnoty databáze.
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
Následující kód přidá vlastní chybovou zprávu pro každý sloupec, který má hodnoty databáze odlišné od toho, co bylo publikováno:OnPostAsync
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
Následující zvýrazněný kód nastaví RowVersion
hodnotu na novou hodnotu načtenou z databáze. Když uživatel příště klikne na Uložit, budou zachyceny pouze chyby souběžnosti, ke kterým dochází od posledního zobrazení stránky Upravit.
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
Příkaz ModelState.Remove
je povinný, protože ModelState
má starou RowVersion
hodnotu. Razor Na stránce ModelState
má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici obě hodnoty.
Aktualizace stránky Upravit
Aktualizujte Pages/Departments/Edit.cshtml
následujícím kódem:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-group">
<label>RowVersion</label>
@Model.Department.RowVersion[7]
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label"></label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label"></label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label"></label>
<input asp-for="Department.StartDate" class="form-control" />
<span asp-validation-for="Department.StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Předchozí kód:
- Aktualizuje direktivu
page
od@page
@page "{id:int}"
- Přidá skrytou verzi řádku.
RowVersion
musí být přidán, takže postback sváže hodnotu. - Zobrazí poslední bajt
RowVersion
pro účely ladění. ViewData
Nahrazuje silným typemInstructorNameSL
.
Konflikty souběžnosti testů se stránkou Upravit
Otevřete dvě instance prohlížeče pro úpravy v anglickém oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Pro anglické oddělení a vyberte Otevřít v nové kartě.
- Na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte název na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Změňte jiné pole na druhé kartě prohlížeče.
Klikněte na Uložit. Zobrazí se chybové zprávy pro všechna pole, která neodpovídají hodnotám databáze:
Toto okno prohlížeče nemělo v úmyslu změnit pole Název. Zkopírujte a vložte aktuální hodnotu (Jazyky) do pole Název. Vysouvte tabulátor. Ověření na straně klienta odebere chybovou zprávu.
Znovu klikněte na Uložit . Hodnota, kterou jste zadali na druhé kartě prohlížeče, se uloží. Uložené hodnoty se zobrazí na stránce Index.
Aktualizace modelu odstranění stránky
Aktualizujte Pages/Departments/Delete.cshtml.cs
následujícím kódem:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "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.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}
Stránka Delete detekuje konflikty souběžnosti, když se entita po načtení změnila. Department.RowVersion
je verze řádku, když byla entita načtena. Při EF Core vytváření příkazu SQL DELETE obsahuje klauzuli WHERE s RowVersion
. Pokud výsledkem příkazu SQL DELETE je nula řádků:
- Příkaz
RowVersion
SQL DELETE se v databázi neshodujeRowVersion
. - Vyvolá se výjimka DbUpdateConcurrencyException.
OnGetAsync
je volána sconcurrencyError
.
Aktualizace stránky Odstranit
Aktualizujte Pages/Departments/Delete.cshtml
následujícím kódem:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.RowVersion)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.RowVersion[7])
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Administrator.FullName)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</div>
</form>
</div>
Předchozí kód provede následující změny:
- Aktualizuje direktivu
page
od@page
@page "{id:int}"
- Přidá chybovou zprávu.
- Nahradí Jméno FirstMidName úplným názvem v poli Správce .
- Změny
RowVersion
pro zobrazení posledního bajtu - Přidá skrytou verzi řádku.
RowVersion
musí být přidán, takže postback sváže hodnotu.
Konflikty souběžnosti testů
Vytvoření testovacího oddělení
Otevřete dvě instance odstranění v testovacím oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro testovací oddělení a vyberte Otevřít na nové kartě.
- Klikněte na odkaz Upravit hypertextový odkaz pro testovací oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte rozpočet na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Odstraňte testovací oddělení z druhé karty. Zobrazí se chyba souběžnosti s aktuálními hodnotami z databáze. Kliknutím na Odstranit odstraníte entitu, pokud RowVersion
nebyla aktualizována.
Další materiály
Další kroky
Toto je poslední kurz série. Další témata jsou popsána ve verzi MVC této série kurzů.
V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje entitu souběžně (ve stejnou dobu). Pokud narazíte na problémy, které nemůžete vyřešit, stáhněte nebo zobrazte dokončenou aplikaci. Pokyny ke stažení
Konflikty souběžnosti
Ke konfliktu souběžnosti dochází v případě, že:
- Uživatel přejde na stránku pro úpravy entity.
- Jiný uživatel aktualizuje stejnou entitu před zápisem změny prvního uživatele do databáze.
Pokud detekce souběžnosti není povolená, když dojde k souběžné aktualizaci:
- Poslední aktualizace vyhrává. To znamená, že poslední hodnoty aktualizace se uloží do databáze.
- První zaktuálních
Optimistická metoda souběžného zpracování
Optimistická souběžnost umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reaguje, když ano. Jane například navštíví stránku pro úpravy oddělení a změní rozpočet pro anglické oddělení z 350 000,00 USD na 0,00 USD.
Než Jane klikne na Uložit, jan navštíví stejnou stránku a změní pole Počáteční datum od 1. 9. 2007 do 1. 9. 2013.
Jane nejprve klikne na Uložit a zobrazí se její změna, když prohlížeč zobrazí stránku Rejstřík.
Jan klikne na uložit na stránce Pro úpravy, která stále zobrazuje rozpočet 350 000,00 USD. Co se stane dál, určuje způsob zpracování konfliktů souběžnosti.
Optimistická souběžnost zahrnuje následující možnosti:
Můžete sledovat, kterou vlastnost uživatel upravil, a aktualizovat pouze odpovídající sloupce v databázi.
V tomto scénáři nedojde ke ztrátě žádných dat. Dva uživatelé aktualizovali různé vlastnosti. Když někdo příště přejde do anglického oddělení, uvidí změny Jane i Johna. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat. Tento přístup:
- Nelze se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti provedeny konkurenční změny.
- Obecně není praktické ve webové aplikaci. Vyžaduje udržování významného stavu, aby bylo možné sledovat všechny načtené hodnoty a nové hodnoty. Udržování velkého objemu stavu může ovlivnit výkon aplikace.
- Může zvýšit složitost aplikace v porovnání s detekcí souběžnosti u entity.
Johnovi můžete dát možnost přepsat Janovu změnu.
Když někdo příště přejde do anglického oddělení, uvidí 1. 9. 2013 a načte hodnotu 350 000,00 USD. Tento přístup se označuje jako klient wins nebo last ve scénáři Wins . (Všechny hodnoty z klienta mají přednost před tím, co je v úložišti dat.) Pokud neprovádíte žádné kódování pro zpracování souběžnosti, dojde k automatickému zpracování služby Client Wins.
Můžete zabránit tomu, aby se v databázi aktualizovala změna Johna. Aplikace obvykle:
- Zobrazí chybovou zprávu.
- Zobrazí aktuální stav dat.
- Umožňuje uživateli znovu použít změny.
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, že se bez upozornění uživatele nepřepíší žádné změny.
Zpracování souběžnosti
Když je vlastnost nakonfigurovaná jako token souběžnosti:
- EF Core Ověřuje, že vlastnost nebyla po načtení změněna. Kontrola nastane, když SaveChanges nebo SaveChangesAsync je volána.
- Pokud byla vlastnost po načtení změněna, DbUpdateConcurrencyException vyvolá se.
Databáze a datový model musí být nakonfigurovány tak, aby podporovaly vyvolání DbUpdateConcurrencyException
.
Zjišťování konfliktů souběžnosti u vlastnosti
Konflikty souběžnosti lze zjistit na úrovni vlastnosti pomocí atributu ConcurrencyCheck . Atribut lze použít na více vlastností modelu. Další informace naleznete v tématu Datové poznámky- SouběžnostCheck.
Tento [ConcurrencyCheck]
atribut se v tomto kurzu nepoužívá.
Zjišťování konfliktů souběžnosti na řádku
Pokud chcete zjistit konflikty souběžnosti, přidá se do modelu sloupec sledování rowversion . rowversion
:
- Je specifický pro SQL Server. Jiné databáze nemusí poskytovat podobnou funkci.
- Slouží k určení, že se entita od načtení z databáze nezměnila.
Databáze vygeneruje sekvenční rowversion
číslo, které se při každé aktualizaci řádku zvýší. Update
V příkazu nebo Delete
Where
příkaz obsahuje klauzule načtenou hodnotu rowversion
. Pokud se řádek, který se aktualizuje, změnil:
rowversion
neodpovídá načtené hodnotě.Delete
PříkazyUpdate
nenaleznou řádek, protožeWhere
klauzule obsahuje načtenýrowversion
.- A
DbUpdateConcurrencyException
je vyhozen.
Pokud EF Corepříkaz nebo Delete
příkaz neaktualizoval Update
žádné řádky, vyvolá se výjimka souběžnosti.
Přidání vlastnosti sledování do entity Oddělení
V Models/Department.cs
aplikaci přidejte sledovací vlastnost s názvem RowVersion:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
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)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Atribut časového razítka určuje, že tento sloupec je součástí Where
klauzule Update
a Delete
příkazů. Atribut je volán Timestamp
, protože předchozí verze SQL Serveru používaly datový typ SQL timestamp
před tím, než ho typ SQL rowversion
nahradil.
Fluent API může také zadat vlastnost sledování:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
Následující kód ukazuje část T-SQL vygenerovanou při EF Core aktualizaci názvu oddělení:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Předchozí zvýrazněný kód zobrazuje klauzuli WHERE
obsahující RowVersion
. Pokud se databáze RowVersion
nerovná parametru (@p2
), nebudou RowVersion
aktualizovány žádné řádky.
Následující zvýrazněný kód ukazuje T-SQL, který ověřuje, že byl aktualizován přesně jeden řádek:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT vrátí počet řádků ovlivněných posledním příkazem. V žádném řádku nejsou aktualizoványDbUpdateConcurrencyException
, EF Core vyvolá chybu .
T-SQL EF Core se zobrazí v okně výstupu sady Visual Studio.
Aktualizace databáze
Přidáním vlastnosti se RowVersion
změní model databáze, který vyžaduje migraci.
Sestavte projekt. Do příkazového okna zadejte následující:
dotnet ef migrations add RowVersion
dotnet ef database update
Předchozí příkazy:
Migrations/{time stamp}_RowVersion.cs
Přidá soubor migrace.Migrations/SchoolContextModelSnapshot.cs
Aktualizuje soubor. Aktualizace přidá doBuildModel
metody následující zvýrazněný kód:Spustí migrace pro aktualizaci databáze.
Generování modelu oddělení
Postupujte podle pokynů vygenerování modelu studenta a použijte Department
ho pro třídu modelu.
Předchozí příkaz vygeneruje Department
model. Otevřete projekt v sadě Visual Studio.
Sestavte projekt.
Aktualizace indexové stránky Oddělení
Modul generování uživatelského rozhraní vytvořil RowVersion
sloupec pro indexovou stránku, ale toto pole by se nemělo zobrazovat. V tomto kurzu se zobrazí poslední bajt, RowVersion
který vám pomůže porozumět souběžnosti. Poslední bajt není zaručený jako jedinečný. Skutečná aplikace by se nezobrazila RowVersion
ani poslední bajt RowVersion
.
Aktualizujte indexovou stránku:
- Nahraďte index odděleními.
- Nahraďte značky obsahující
RowVersion
poslední bajt .RowVersion
- Nahraďte FirstMidName úplným názvem.
Aktualizovaná stránka zobrazuje následující kód:
@page
@model ContosoUniversity.Pages.Departments.IndexModel
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Administrator)
</th>
<th>
RowVersion
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department) {
<tr>
<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>
<td>
@item.RowVersion[7]
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Aktualizace modelu upravit stránku
Aktualizujte Pages/Departments/Edit.cshtml.cs
následujícím kódem:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
// null means Department was deleted by another user.
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Update the RowVersion to the value when this entity was
// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private IActionResult HandleDeletedDepartment()
{
// ModelState contains the posted data because of the deletion error and will overide the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task SetDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
}
}
Pokud chcete zjistit problém se souběžností, aktualizuje rowVersion
se OriginalValue hodnota z entity, kterou byla načtena. EF Core vygeneruje příkaz SQL UPDATE s klauzulí WHERE obsahující původní RowVersion
hodnotu. Pokud příkaz UPDATE neovlivní žádné řádky (žádné řádky nemají původní RowVersion
hodnotu), DbUpdateConcurrencyException
vyvolá se výjimka.
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
// null means Department was deleted by another user.
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Update the RowVersion to the value when this entity was
// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
V předchozím kódu je hodnota při Department.RowVersion
načtení entity. OriginalValue
je hodnota v databázi, když FirstOrDefaultAsync
byla volána v této metodě.
Následující kód získá hodnoty klienta (hodnoty publikované do této metody) a hodnoty databáze:
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
Následující kód přidá vlastní chybovou zprávu pro každý sloupec, který má hodnoty databáze odlišné od toho, co bylo publikováno:OnPostAsync
private async Task SetDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
Následující zvýrazněný kód nastaví RowVersion
hodnotu na novou hodnotu načtenou z databáze. Když uživatel příště klikne na Uložit, budou zachyceny pouze chyby souběžnosti, ke kterým dochází od posledního zobrazení stránky Upravit.
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
Příkaz ModelState.Remove
je povinný, protože ModelState
má starou RowVersion
hodnotu. Razor Na stránce ModelState
má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici obě hodnoty.
Aktualizace stránky Upravit
Aktualizujte Pages/Departments/Edit.cshtml
následujícím kódem:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-group">
<label>RowVersion</label>
@Model.Department.RowVersion[7]
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label"></label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label"></label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label"></label>
<input asp-for="Department.StartDate" class="form-control" />
<span asp-validation-for="Department.StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Předchozí kód:
- Aktualizuje direktivu
page
od@page
@page "{id:int}"
- Přidá skrytou verzi řádku.
RowVersion
musí být přidán, takže post back binds hodnotu. - Zobrazí poslední bajt
RowVersion
pro účely ladění. ViewData
Nahrazuje silným typemInstructorNameSL
.
Konflikty souběžnosti testů se stránkou Upravit
Otevřete dvě instance prohlížeče pro úpravy v anglickém oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Pro anglické oddělení a vyberte Otevřít v nové kartě.
- Na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte název na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Změňte jiné pole na druhé kartě prohlížeče.
Klikněte na Uložit. Zobrazí se chybové zprávy pro všechna pole, která neodpovídají hodnotám databáze:
Toto okno prohlížeče nemělo v úmyslu změnit pole Název. Zkopírujte a vložte aktuální hodnotu (Jazyky) do pole Název. Vysouvte tabulátor. Ověření na straně klienta odebere chybovou zprávu.
Znovu klikněte na Uložit . Hodnota, kterou jste zadali na druhé kartě prohlížeče, se uloží. Uložené hodnoty se zobrazí na stránce Index.
Aktualizace stránky Odstranit
Aktualizujte model stránky Delete následujícím kódem:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "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.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}
Stránka Delete detekuje konflikty souběžnosti, když se entita po načtení změnila. Department.RowVersion
je verze řádku, když byla entita načtena. Při EF Core vytváření příkazu SQL DELETE obsahuje klauzuli WHERE s RowVersion
. Pokud výsledkem příkazu SQL DELETE je nula řádků:
- Příkaz
RowVersion
SQL DELETE se v databázi neshodujeRowVersion
. - Vyvolá se výjimka DbUpdateConcurrencyException.
OnGetAsync
je volána sconcurrencyError
.
Aktualizace stránky Odstranit
Aktualizujte Pages/Departments/Delete.cshtml
následujícím kódem:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.RowVersion)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.RowVersion[7])
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Administrator.FullName)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</div>
</form>
</div>
Předchozí kód provede následující změny:
- Aktualizuje direktivu
page
od@page
@page "{id:int}"
- Přidá chybovou zprávu.
- Nahradí Jméno FirstMidName úplným názvem v poli Správce .
- Změny
RowVersion
pro zobrazení posledního bajtu - Přidá skrytou verzi řádku.
RowVersion
musí být přidán, takže post back binds hodnotu.
Test souběžnosti koliduje se stránkou Odstranit
Vytvoření testovacího oddělení
Otevřete dvě instance odstranění v testovacím oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro testovací oddělení a vyberte Otevřít na nové kartě.
- Klikněte na odkaz Upravit hypertextový odkaz pro testovací oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte rozpočet na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Odstraňte testovací oddělení z druhé karty. Zobrazí se chyba souběžnosti s aktuálními hodnotami z databáze. Kliknutím na Odstranit odstraníte entitu, pokud RowVersion
nebyla aktualizována.
Přečtěte si téma Dědičnost , jak dědit datový model.